mirror of https://github.com/portainer/portainer
feat(a11y): add labels and roles [EE-6717] (#11181)
parent
6c89d3c0c9
commit
ce3a1b8ba5
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -184,6 +184,7 @@ export const ngModule = angular
|
|||
'components',
|
||||
'isLoading',
|
||||
'noOptionsMessage',
|
||||
'aria-label',
|
||||
])
|
||||
)
|
||||
.component(
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
|
|
|
@ -188,6 +188,7 @@ export function Datatable<D extends DefaultType>({
|
|||
isLoading={isLoading}
|
||||
onSortChange={handleSortChange}
|
||||
data-cy={dataCy}
|
||||
aria-label={`${title} table`}
|
||||
/>
|
||||
|
||||
<DatatableFooter
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -24,6 +24,7 @@ export function TasksDatatable({
|
|||
dataset={dataset}
|
||||
search={search}
|
||||
emptyContentLabel="No task matching filter."
|
||||
aria-label="Tasks table"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
|
|
|
@ -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)}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue