feat(a11y): add labels and roles [EE-6717] (#11181)

pull/11226/head
Chaim Lev-Ari 2024-02-19 16:37:26 +02:00 committed by GitHub
parent 6c89d3c0c9
commit ce3a1b8ba5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 72 additions and 30 deletions

View File

@ -209,7 +209,7 @@
</uib-tab>
<uib-tab index="1" disable="!buildLogs">
<uib-tab-heading class="vertical-center"> <pr-icon icon="'file-text'" class="leading-none"></pr-icon> Output </uib-tab-heading>
<pre class="log_viewer">
<pre class="log_viewer" data-cy="logViewer">
<div ng-repeat="line in buildLogs track by $index" class="line"><p class="inner_line" ng-click="active=!active" ng-class="{'line_selected': active}">{{ line }}</p></div>
<div ng-if="!buildLogs.length" class="line"><p class="inner_line">No build output available.</p></div>
</pre>

View File

@ -1,5 +1,5 @@
<!-- helm chart -->
<div ng-class="{ 'blocklist-item--selected': $ctrl.model.Selected }" class="blocklist-item template-item mx-0" ng-click="$ctrl.onSelect($ctrl.model)">
<div ng-class="{ 'blocklist-item--selected': $ctrl.model.Selected }" class="blocklist-item template-item mx-0" ng-click="$ctrl.onSelect($ctrl.model)" role="listitem">
<div class="blocklist-item-box">
<!-- helmchart-image -->
<span class="shrink-0">

View File

@ -30,7 +30,7 @@
></beta-alert>
</div>
<div class="blocklist !px-0">
<div class="blocklist !px-0" role="list">
<helm-templates-list-item
ng-repeat="chart in allCharts = ($ctrl.charts | filter:$ctrl.state.textFilter | filter: $ctrl.state.selectedCategory)"
model="chart"

View File

@ -184,6 +184,7 @@ export const ngModule = angular
'components',
'isLoading',
'noOptionsMessage',
'aria-label',
])
)
.component(

View File

@ -69,10 +69,13 @@ export function Badge({
children,
}: PropsWithChildren<Props>) {
const baseClasses =
'flex w-fit items-center !text-xs font-medium rounded-full px-2 py-0.5';
'inline-flex w-fit items-center !text-xs font-medium rounded-full px-2 py-0.5';
return (
<span className={clsx(baseClasses, typeClasses[type], className)}>
<span
className={clsx(baseClasses, typeClasses[type], className)}
role="status"
>
{children}
</span>
);

View File

@ -28,6 +28,7 @@ export function BlocklistItem<T extends ElementType>({
'blocklist-item--selected': isSelected,
}
)}
role="listitem"
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
>

View File

@ -188,6 +188,7 @@ export function Datatable<D extends DefaultType>({
isLoading={isLoading}
onSortChange={handleSortChange}
data-cy={dataCy}
aria-label={`${title} table`}
/>
<DatatableFooter

View File

@ -11,6 +11,7 @@ interface Props<D extends DefaultType> extends AutomationTestingProps {
onSortChange?(colId: string, desc: boolean): void;
isLoading?: boolean;
emptyContentLabel?: string;
'aria-label'?: string;
}
export function DatatableContent<D extends DefaultType>({
@ -20,12 +21,13 @@ export function DatatableContent<D extends DefaultType>({
isLoading,
emptyContentLabel,
'data-cy': dataCy,
'aria-label': ariaLabel,
}: Props<D>) {
const headerGroups = tableInstance.getHeaderGroups();
const pageRowModel = tableInstance.getPaginationRowModel();
return (
<Table data-cy={dataCy} className="nowrap-cells">
<Table data-cy={dataCy} className="nowrap-cells" aria-label={ariaLabel}>
<thead>
{headerGroups.map((headerGroup) => (
<Table.HeaderRow<D>

View File

@ -28,6 +28,8 @@ interface Props<D extends DefaultType> {
* keyword to filter by
*/
search?: string;
'aria-label'?: string;
}
export function NestedDatatable<D extends DefaultType>({
@ -39,6 +41,7 @@ export function NestedDatatable<D extends DefaultType>({
isLoading,
initialSortBy,
search,
'aria-label': ariaLabel,
}: Props<D>) {
const tableInstance = useReactTable<D>({
columns,
@ -70,6 +73,7 @@ export function NestedDatatable<D extends DefaultType>({
isLoading={isLoading}
emptyContentLabel={emptyContentLabel}
renderRow={(row) => <Table.Row<D> cells={row.getVisibleCells()} />}
aria-label={ariaLabel}
/>
</Table.Container>
</NestedTable>

View File

@ -42,6 +42,7 @@ export function SearchBar({
onChange={(e) => setSearchValue(e.target.value)}
placeholder={placeholder}
data-cy={dataCy}
aria-label="Search input"
/>
{children}
<Button onClick={onClear} icon={X} color="none" disabled={!searchValue} />

View File

@ -1,5 +1,5 @@
import clsx from 'clsx';
import { PropsWithChildren } from 'react';
import { AriaAttributes, PropsWithChildren } from 'react';
import { AutomationTestingProps } from '@/types';
@ -14,23 +14,21 @@ import { TableHeaderCell } from './TableHeaderCell';
import { TableHeaderRow } from './TableHeaderRow';
import { TableRow } from './TableRow';
interface Props extends AutomationTestingProps {
interface Props extends AutomationTestingProps, AriaAttributes {
className?: string;
}
function MainComponent({
children,
className,
'data-cy': dataCy,
...props
}: PropsWithChildren<Props>) {
return (
<div className="table-responsive">
<table
data-cy={dataCy}
className={clsx(
'table-hover table-filters nowrap-cells table',
className
)}
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
className={clsx('table-hover table-filters table', className)}
>
{children}
</table>

View File

@ -23,7 +23,7 @@ export function TableTitle({
<>
<div className={clsx('toolBar flex-col', className)} id={id}>
<div className="flex w-full items-center gap-1 p-0">
<div className="toolBarTitle">
<h2 className="toolBarTitle m-0 text-2xl">
{icon && (
<div className="widget-icon">
<Icon icon={icon} className="space-right" />
@ -31,7 +31,7 @@ export function TableTitle({
)}
{label}
</div>
</h2>
{children}
</div>
</div>

View File

@ -17,6 +17,7 @@ export function buildExpandColumn<T extends DefaultType>(): ColumnDef<T> {
onClick={table.getToggleAllRowsExpandedHandler()}
color="none"
icon={table.getIsAllRowsExpanded() ? ChevronDown : ChevronUp}
title="Expand all"
/>
)
);
@ -32,6 +33,7 @@ export function buildExpandColumn<T extends DefaultType>(): ColumnDef<T> {
}}
color="none"
icon={row.getIsExpanded() ? ChevronDown : ChevronUp}
title={row.getIsExpanded() ? 'Collapse' : 'Expand'}
/>
),
enableColumnFilter: false,

View File

@ -4,6 +4,7 @@ import {
SelectComponentsConfig,
} from 'react-select';
import _ from 'lodash';
import { AriaAttributes } from 'react';
import { AutomationTestingProps } from '@/types';
@ -20,7 +21,9 @@ type Options<TValue> = OptionsOrGroups<
GroupBase<Option<TValue>>
>;
interface SharedProps extends AutomationTestingProps {
interface SharedProps
extends AutomationTestingProps,
Pick<AriaAttributes, 'aria-label'> {
name?: string;
inputId?: string;
placeholder?: string;
@ -87,6 +90,8 @@ export function SingleSelect<TValue = string>({
components,
isLoading,
noOptionsMessage,
isMulti,
...aria
}: SingleProps<TValue>) {
const selectedValue =
value || (typeof value === 'number' && value === 0)
@ -111,6 +116,8 @@ export function SingleSelect<TValue = string>({
components={components}
isLoading={isLoading}
noOptionsMessage={noOptionsMessage}
// eslint-disable-next-line react/jsx-props-no-spreading
{...aria}
/>
);
}
@ -152,6 +159,7 @@ export function MultiSelect<TValue = string>({
components,
isLoading,
noOptionsMessage,
...aria
}: Omit<MultiProps<TValue>, 'isMulti'>) {
const selectedOptions = findSelectedOptions(options, value);
return (
@ -174,6 +182,8 @@ export function MultiSelect<TValue = string>({
components={components}
isLoading={isLoading}
noOptionsMessage={noOptionsMessage}
// eslint-disable-next-line react/jsx-props-no-spreading
{...aria}
/>
);
}

View File

@ -16,5 +16,11 @@ export function NestedNetworksDatatable({
const isSwarm = useIsSwarm(environmentId);
const columns = useColumns(isSwarm);
return <NestedDatatable columns={columns} dataset={dataset} />;
return (
<NestedDatatable
columns={columns}
dataset={dataset}
aria-label="Networks table"
/>
);
}

View File

@ -1,6 +1,7 @@
import { truncate } from '@/portainer/filters/filters';
import { Link } from '@@/Link';
import { Badge } from '@@/Badge';
import { columnHelper } from './helper';
@ -18,12 +19,9 @@ export const name = columnHelper.accessor('Name', {
{truncate(item.Name, 40)}
</Link>
{item.ResourceControl?.System && (
<span
style={{ marginLeft: '10px' }}
className="label label-info image-tag space-left"
>
<Badge type="info" className="ml-2">
System
</span>
</Badge>
)}
</>
);

View File

@ -24,6 +24,7 @@ export function TasksDatatable({
dataset={dataset}
search={search}
emptyContentLabel="No task matching filter."
aria-label="Tasks table"
/>
);
}

View File

@ -108,7 +108,9 @@ function Cell({
</Authorized>
)}
{item.dangling && (
<span className="label label-warning image-tag ml-2">Unused</span>
<span className="label label-warning image-tag ml-2" role="status">
Unused
</span>
)}
</>
);

View File

@ -212,6 +212,7 @@ export function EnvironmentList({ onClickBrowse, onRefresh }: Props) {
<div
className="blocklist mt-5 !space-y-2 !p-0"
data-cy="home-endpointList"
role="list"
>
{renderItems(
isLoading,

View File

@ -31,9 +31,10 @@ export function RefField({
stackId,
}: Props) {
const [inputValue, updateInputValue] = useStateWrapper(value, onChange);
const inputId = 'repository-reference-field';
return isBE ? (
<Wrapper
inputId={inputId}
errors={error}
tip={
<>
@ -44,6 +45,7 @@ export function RefField({
}
>
<RefSelector
inputId={inputId}
value={value}
onChange={onChange}
model={model}
@ -53,6 +55,7 @@ export function RefField({
</Wrapper>
) : (
<Wrapper
inputId={inputId}
errors={error}
tip={
<>
@ -65,6 +68,7 @@ export function RefField({
}
>
<Input
id={inputId}
value={inputValue}
onChange={(e) => updateInputValue(e.target.value)}
placeholder="refs/heads/main"
@ -77,7 +81,8 @@ function Wrapper({
tip,
children,
errors,
}: PropsWithChildren<{ tip: ReactNode; errors?: string }>) {
inputId,
}: PropsWithChildren<{ tip: ReactNode; errors?: string; inputId: string }>) {
return (
<div className="form-group">
<span className="col-sm-12 mb-2">
@ -86,7 +91,7 @@ function Wrapper({
<div className="col-sm-12">
<FormControl
label="Repository reference"
inputId="stack_repository_reference_name"
inputId={inputId}
required
errors={errors}
>

View File

@ -13,12 +13,14 @@ export function RefSelector({
onChange,
isUrlValid,
stackId,
inputId,
}: {
model: RefFieldModel;
value: string;
stackId?: StackId;
onChange: (value: string) => void;
isUrlValid?: boolean;
inputId: string;
}) {
const creds = getAuthentication(model);
const payload = {
@ -64,6 +66,7 @@ export function RefSelector({
return (
<PortainerSelect
inputId={inputId}
value={value}
options={refs || [{ value: 'refs/heads/main', label: 'refs/heads/main' }]}
onChange={(e) => e && onChange(e)}

View File

@ -86,7 +86,7 @@ export function AppTemplatesList({
}
/>
<div className="blocklist gap-y-2 !px-[20px] !pb-[20px]">
<div className="blocklist gap-y-2 !px-[20px] !pb-[20px]" role="list">
{pagedTemplates.map((template) => (
<AppTemplatesListItem
key={template.Id}

View File

@ -55,6 +55,7 @@ export function Filters({
value={listState.category}
bindToBody
isClearable
aria-label="Category filter"
/>
</div>
)}
@ -71,6 +72,7 @@ export function Filters({
value={listState.types}
bindToBody
isClearable
aria-label="Type filter"
/>
</div>
)}
@ -83,6 +85,7 @@ export function Filters({
options={orderByFields}
placeholder="Sort By"
value={listState.sortBy}
aria-label="Sort"
/>
</div>
</div>

View File

@ -63,7 +63,7 @@ export function CustomTemplatesList({
)}
/>
<div className="blocklist gap-y-2 !px-[20px] !pb-[20px]">
<div className="blocklist gap-y-2 !px-[20px] !pb-[20px]" role="list">
{pagedTemplates.map((template) => (
<CustomTemplatesListItem
key={template.Id}

View File

@ -182,7 +182,7 @@ export function DockerSidebar({ environmentId, environment }: Props) {
icon={setupSubMenuProps.icon}
to={setupSubMenuProps.to}
params={{ endpointId: environmentId }}
data-cy="portainerSidebar-host"
data-cy="portainerSidebar-host-area"
>
<SidebarItem
label="Details"