Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(target-size-automation): Update target size requirement to be assisted #7331

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
import { targetSizeColumnRenderer } from 'assessments/pointer-motion/target-size-column-renderer';
import { InstanceTableRow } from 'assessments/types/instance-table-data';
import { ColumnValueBag } from 'common/types/property-bag/column-value-bag';
import { PropertyBagColumnRendererConfig } from 'common/types/property-bag/property-bag-column-renderer-config';

export class TargetSizeColumnRendererFactory {
public static getColumnComponent<TPropertyBag extends ColumnValueBag>(
configs: PropertyBagColumnRendererConfig<TPropertyBag>[],
): (item: InstanceTableRow<TPropertyBag>) => JSX.Element {
return item => {
return targetSizeColumnRenderer(item, configs);
};
}
}
192 changes: 192 additions & 0 deletions src/assessments/pointer-motion/target-size-column-renderer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
import * as Markup from 'assessments/markup';
import { InstanceTableRow } from 'assessments/types/instance-table-data';
import { ColumnValueBag } from 'common/types/property-bag/column-value-bag';
import { PropertyBagColumnRendererConfig } from 'common/types/property-bag/property-bag-column-renderer-config';
import { TargetSizePropertyBag } from 'common/types/property-bag/target-size-property-bag';
import * as React from 'react';
import { DictionaryStringTo } from 'types/common-types';
import { PropertyBagColumnRendererFactory } from '../common/property-bag-column-renderer-factory';

export function targetSizeColumnRenderer<TPropertyBag extends ColumnValueBag>(
item: InstanceTableRow<any>,
configs: PropertyBagColumnRendererConfig<TPropertyBag>[],
): JSX.Element {
const propertyBag = item.instance.propertyBag;
const renderColumnComponent = (
propertyBag: TargetSizePropertyBag,
columnComponent: (props: TargetSizePropertyBag) => JSX.Element,
) => {
return columnComponent(propertyBag);
};

if (
propertyBag.sizeStatus &&
hasNeededProperties(['height', 'width', 'minSize'], propertyBag)
) {
propertyBag.sizeMessageKey = propertyBag.sizeMessageKey || 'default';

propertyBag.sizeComponent = (
<span id="target-size" className="expanded-property-div">
{renderColumnComponent(
propertyBag,
getTargetSizeMessageComponentFromPropertyBag(propertyBag),
)}
</span>
);
}
if (
propertyBag.offsetStatus &&
hasNeededProperties(['closestOffset', 'minOffset'], propertyBag)
) {
propertyBag.offsetMessageKey = propertyBag.offsetMessageKey || 'default';
propertyBag.offsetComponent = (
<span id="target-offset" className="expanded-property-div">
{renderColumnComponent(
propertyBag,
getTargetOffsetMessageComponentFromPropertyBag(propertyBag),
)}
</span>
);
}

const propertyBagRenderer = PropertyBagColumnRendererFactory.getRenderer<TPropertyBag>(configs);

return propertyBagRenderer(item);
}

function hasNeededProperties(
neededProperties: string[],
propertyBag: TargetSizePropertyBag,
): boolean {
return !neededProperties.some(key => propertyBag.hasOwnProperty(key) === false);
}

export function getTargetSizeMessageComponentFromPropertyBag(
propertyBag: TargetSizePropertyBag,
): (props: TargetSizePropertyBag) => JSX.Element {
return statusWithMessageKeyToMessageComponentMapping.size[propertyBag.sizeStatus][
propertyBag.sizeMessageKey
];
}

export function getTargetOffsetMessageComponentFromPropertyBag(
propertyBag: TargetSizePropertyBag,
): (props: TargetSizePropertyBag) => JSX.Element {
return statusWithMessageKeyToMessageComponentMapping.offset[propertyBag.offsetStatus][
propertyBag.offsetMessageKey
];
}

const statusWithMessageKeyToMessageComponentMapping: DictionaryStringTo<
DictionaryStringTo<DictionaryStringTo<(props: TargetSizePropertyBag) => JSX.Element>>
> = {
size: {
pass: {
default: props => (
<>
Element has sufficient touch target size ({props.height}px by {props.width}
px).
</>
),
obscured: props => (
<>Element was ignored because it is fully obscured and not clickable.</>
),
large: props => <>Element has sufficient touch target size.</>,
},
fail: {
default: props => (
<>
Element has <Markup.Term>insufficient</Markup.Term> touch target size (
{props.height}px by {props.width}
px, should be at least {props.minSize}px by {props.minSize}px){' '}
</>
),
partiallyObscured: props => (
<>
Element has <Markup.Term>insufficient</Markup.Term> touch target size (
{props.height}px by {props.width}
px, should be at least {props.minSize}px by {props.minSize}px) because it is
partially obscured.
</>
),
},
incomplete: {
default: props => (
<>
Element has negative tabindex with <Markup.Term>insufficient</Markup.Term> touch
target size ({props.height}px by {props.width}px, should be at least{' '}
{props.minSize}px by
{props.minSize}px). This <Markup.Term>may be OK</Markup.Term> if the element is
not a touch target.
</>
),
contentOverflow: props => (
<>
Element touch target size{' '}
<Markup.Term>could not be accurately determined</Markup.Term> due to overflow
content.
</>
),
partiallyObscured: props => (
<>
Element with negative tabindex has
<Markup.Term>insufficient</Markup.Term> touch target size because it is
partially obscured. This <Markup.Term>may be OK</Markup.Term> if the element is
not a touch target.
</>
),
partiallyObscuredNonTabbable: props => (
<>
Element has <Markup.Term>insufficient</Markup.Term>
touch target size because it is partially obscured by a neighbor with negative
tabindex. This <Markup.Term>may be OK</Markup.Term> if the neighbor is not a
touch target.
</>
),
tooManyRects: props => (
<>
<Markup.Term>Could not determine element target size</Markup.Term>
because there are too many overlapping elements.
</>
),
},
},
offset: {
pass: {
default: props => (
<>
Element has sufficient offset from its closest neighbor ({props.closestOffset}
px)
</>
),
large: props => <>Element has sufficient offset from its closest neighbor.</>,
},
fail: {
default: props => (
<>
Element has <Markup.Term>insufficient</Markup.Term> offset to its closest
neighbor ({props.closestOffset}
px in diameter, should be at least {props.minOffset}px in diameter)
</>
),
},
incomplete: {
default: props => (
<>
Element with negative tabindex has
<Markup.Term>insufficient</Markup.Term> offset to its closest neighbor. This{' '}
<Markup.Term>may be OK</Markup.Term> if the element is not a touch target.
</>
),
nonTabbableNeighbor: props => (
<>
Element has <Markup.Term>sufficient</Markup.Term> offset from its closest
neighbor and the closest neighbor has a negative tabindex. This{' '}
<Markup.Term>may be OK</Markup.Term> if the neighbor is not a touch target.
</>
),
},
},
};
128 changes: 108 additions & 20 deletions src/assessments/pointer-motion/test-steps/target-size.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,24 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
import { AnalyzerConfigurationFactory } from 'assessments/common/analyzer-configuration-factory';
import {
getTargetOffsetMessageComponentFromPropertyBag,
getTargetSizeMessageComponentFromPropertyBag,
} from 'assessments/pointer-motion/target-size-column-renderer';
import { TargetSizeColumnRendererFactory } from 'assessments/pointer-motion/target-size-column-renderer-factory';
import { ReportInstanceField } from 'assessments/types/report-instance-field';
import { ChecksType } from 'background/assessment-data-converter';
import { TargetSizePropertyBag } from 'common/types/property-bag/target-size-property-bag';
import { DecoratedAxeNodeResult } from 'common/types/store-data/visualization-scan-result-data';
import { link } from 'content/link';
import * as content from 'content/test/pointer-motion/target-size';
import { AssessmentVisualizationEnabledToggle } from 'DetailsView/components/assessment-visualization-enabled-toggle';
import { AnalyzerConfiguration } from 'injected/analyzers/analyzer';
import { AnalyzerProvider } from 'injected/analyzers/analyzer-provider';
import { ScannerUtils } from 'injected/scanner-utils';
import { isEmpty } from 'lodash';
import * as React from 'react';
import { PropertyBagColumnRendererConfig } from '../../../common/types/property-bag/property-bag-column-renderer-config';
import { ManualTestRecordYourResults } from '../../common/manual-test-record-your-results';
import * as Markup from '../../markup';
import { Requirement } from '../../types/requirement';
Expand All @@ -17,41 +33,37 @@ const description: JSX.Element = (

const howToTest: JSX.Element = (
<div>
<p>
For this requirement, Accessibility Insights for Web highlights non-inline focusable
elements on the target page and checks the touch target size.
</p>
<p>
<Markup.Emphasis>
Note: If no matching/failing instances are found, this requirement will
automatically be marked as pass.
</Markup.Emphasis>
</p>
<ol>
<li>
<p>
Examine the target page to identify interactive elements which have been created
by authors (non-native browser controls).
In the <Markup.Term>Instances</Markup.Term> list below, examine each element,
and verify the element is a <Markup.Term>sufficient size</Markup.Term> and{' '}
<Markup.Term>sufficient offset</Markup.Term> from its neighbor.
</p>
</li>
<li>
<p>
Verify these elements are a minimum size of 24x24 css pixels. The following
exceptions apply:
If an element does not have sufficient size and/or sufficient offset from its
neighbor, verify the following exceptions do not apply:
</p>
<ul>
<li>
<p>
<Markup.Emphasis>Spacing</Markup.Emphasis>: These elements may be
smaller than 24x24 css pixels so long as it is within a 24x24 css pixel
target spacing circle that doesn’t overlap with other targets or their
24x24 target spacing circle.
</p>
</li>
<li>
<p>
<Markup.Emphasis>Equivalent</Markup.Emphasis>: If an alternative control
is provided on the same page that successfully meets the target
criteria.
</p>
</li>
<li>
<p>
<Markup.Emphasis>Inline</Markup.Emphasis>: The target is in a sentence,
or its size is otherwise constrained by the line-height of non-target
text.
</p>
</li>
<li>
<p>
<Markup.Emphasis>User agent control</Markup.Emphasis>: The size of the
Expand All @@ -73,12 +85,88 @@ const howToTest: JSX.Element = (
</div>
);

const displayPropertyBagConfig: PropertyBagColumnRendererConfig<TargetSizePropertyBag>[] = [
{
propertyName: 'sizeComponent',
displayName: 'Size',
defaultValue: null,
},
{
propertyName: 'offsetComponent',
displayName: 'Offset',
defaultValue: null,
},
];

const generateTargetSizePropertyBagFrom = (
ruleResult: DecoratedAxeNodeResult,
checkName: ChecksType,
): TargetSizePropertyBag => {
if (
ruleResult[checkName] &&
!isEmpty(ruleResult[checkName]) &&
ruleResult[checkName].some(r => r.data)
) {
const status =
ruleResult.status === true
? 'pass'
: ruleResult.status === false
? 'fail'
: 'incomplete';
const data = Object.assign(
{},
...ruleResult[checkName].map(r => {
return {
...r.data,
[`${r.id.split('-')[1]}Status`]: status,
[`${r.id.split('-')[1]}MessageKey`]: r.data.messageKey,
};
}),
);
return data;
}
return null;
};
export const TargetSize: Requirement = {
key: PointerMotionTestStep.targetSize,
name: 'Target size',
description,
howToTest,
...content,
isManual: true,
isManual: false,
columnsConfig: [
{
key: 'touch-target-info',
name: 'Touch target info',
onRender:
TargetSizeColumnRendererFactory.getColumnComponent<TargetSizePropertyBag>(
displayPropertyBagConfig,
),
},
],
reportInstanceFields: [
ReportInstanceField.fromPropertyBagFunction<TargetSizePropertyBag>(
'Size',
'sizeComponent',
pb => getTargetSizeMessageComponentFromPropertyBag(pb).toString(),
),
ReportInstanceField.fromPropertyBagFunction<TargetSizePropertyBag>(
'Offset',
'offsetComponent',
pb => getTargetOffsetMessageComponentFromPropertyBag(pb).toString(),
),
],
getAnalyzer: (provider: AnalyzerProvider, analyzerConfig: AnalyzerConfiguration) =>
provider.createRuleAnalyzer(
AnalyzerConfigurationFactory.forScanner({
rules: ['target-size'],
resultProcessor: (scanner: ScannerUtils) => scanner.getAllApplicableInstances,
...analyzerConfig,
}),
),
guidanceLinks: [link.WCAG_2_5_8],
getDrawer: provider => provider.createHighlightBoxDrawer(),
getVisualHelperToggle: props => <AssessmentVisualizationEnabledToggle {...props} />,
generatePropertyBagFrom: generateTargetSizePropertyBagFrom,
// getCompletedRequirementDetailsForTelemetry: labelInNameGetCompletedRequirementDetails,
};
1 change: 1 addition & 0 deletions src/assessments/types/report-instance-field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ const common: { [key in CommonReportInstanceFieldKey]: ReportInstanceField } = {
};

function isValid(value: ColumnValue): ColumnValue {
console.log('valid??', isValid);
if (!value) {
return false;
}
Expand Down
Loading
Loading