Skip to content
Open
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
1 change: 1 addition & 0 deletions site/components/DiffView/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ export default function DiffView(props: Props) {

return (
<Diff
accessibility="screen-reader"
optimizeSelection
viewType={viewType}
diffType={type}
Expand Down
8 changes: 8 additions & 0 deletions src/Diff/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
GutterType,
Provider,
ViewType,
AccessibilityType,
RenderToken,
RenderGutter,
DEFAULT_CONTEXT_VALUE,
Expand All @@ -19,6 +20,8 @@ export type DiffType = 'add' | 'delete' | 'modify' | 'rename' | 'copy';
export interface DiffProps {
diffType: DiffType;
hunks: HunkData[];
tableAriaLabel?: string;
accessibility?: AccessibilityType;
viewType?: ViewType;
gutterType?: GutterType;
generateAnchorID?: (change: ChangeData) => string | undefined;
Expand Down Expand Up @@ -67,6 +70,8 @@ function Diff(props: DiffProps) {
const {
diffType,
hunks,
tableAriaLabel,
accessibility,
optimizeSelection,
className,
hunkClassName = DEFAULT_CONTEXT_VALUE.hunkClassName,
Expand Down Expand Up @@ -158,6 +163,7 @@ function Diff(props: DiffProps) {
const settingsContextValue = useMemo(
(): ContextProps => {
return {
accessibility,
hunkClassName,
lineClassName,
generateLineClassName,
Expand All @@ -178,6 +184,7 @@ function Diff(props: DiffProps) {
};
},
[
accessibility,
codeClassName,
codeEvents,
generateAnchorID,
Expand All @@ -204,6 +211,7 @@ function Diff(props: DiffProps) {
ref={root}
className={classNames('diff', `diff-${viewType}`, className)}
onMouseDown={onTableMouseDown}
aria-label={tableAriaLabel}
>
{cols}
{children(hunks)}
Expand Down
11 changes: 9 additions & 2 deletions src/Hunk/CodeCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,23 @@ export interface CodeCellProps extends HTMLAttributes<HTMLTableCellElement> {
text: string;
tokens: TokenNode[] | null;
renderToken: RenderToken | undefined;
screenReaderLabel?: string;
}

function CodeCell(props: CodeCellProps) {
const {changeKey, text, tokens, renderToken, ...attributes} = props;
const {changeKey, text, tokens, renderToken, screenReaderLabel, ...attributes} = props;
const actualRenderToken: DefaultRenderToken = renderToken
? (token, i) => renderToken(token, defaultRenderToken, i)
: defaultRenderToken;
const srId = screenReaderLabel ? `diff-sr-${changeKey}` : undefined;

return (
<td {...attributes} data-change-key={changeKey}>
<td {...attributes} data-change-key={changeKey} {...(srId && {'aria-labelledby': srId})}>
{screenReaderLabel && (
<span id={srId} className="diff-sr-only">
{screenReaderLabel}
</span>
)}
{
tokens
? (isEmptyToken(tokens) ? ' ' : tokens.map(actualRenderToken))
Expand Down
109 changes: 73 additions & 36 deletions src/Hunk/SplitHunk/SplitChange.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import {mapValues} from 'lodash';
import {ChangeData, getChangeKey} from '../../utils';
import {TokenNode} from '../../tokenize';
import {Side} from '../../interface';
import {RenderToken, RenderGutter, GutterOptions, EventMap, NativeEventMap} from '../../context';
import {RenderToken, RenderGutter, GutterOptions, EventMap, NativeEventMap, AccessibilityType} from '../../context';
import {ChangeSharedProps} from '../interface';
import CodeCell from '../CodeCell';
import {composeCallback, renderDefaultBy, wrapInAnchorBy} from '../utils';
import {getGutterAriaLabel, getCodeCellAriaLabel} from '../accessibility';

const SIDE_OLD = 0;
const SIDE_NEW = 1;
Expand All @@ -33,24 +34,53 @@ function useCallbackOnSide(side: Side, setHover: SetHover, change: ChangeData |
}

interface RenderCellArgs {
change: ChangeData | null;
side: typeof SIDE_OLD | typeof SIDE_NEW;
selected: boolean;
tokens: TokenNode[] | null;
gutterClassName: string;
codeClassName: string;
gutterEvents: NativeEventMap;
codeEvents: NativeEventMap;
anchorID: string | null | undefined;
gutterAnchor: boolean;
gutterAnchorTarget: string | null | undefined;
hideGutter: boolean;
hover: boolean;
renderToken: RenderToken | undefined;
renderGutter: RenderGutter;
change: ChangeData | null;
side: typeof SIDE_OLD | typeof SIDE_NEW;
selected: boolean;
tokens: TokenNode[] | null;
gutterClassName: string;
codeClassName: string;
gutterEvents: NativeEventMap;
codeEvents: NativeEventMap;
anchorID: string | null | undefined;
gutterAnchor: boolean;
gutterAnchorTarget: string | null | undefined;
hideGutter: boolean;
hover: boolean;
renderToken: RenderToken | undefined;
renderGutter: RenderGutter;
accessibility?: AccessibilityType;
}

function renderCells(args: RenderCellArgs) {
type SideName = 'old' | 'new';

function getSideName(side: typeof SIDE_OLD | typeof SIDE_NEW): SideName {
return side === SIDE_OLD ? 'old' : 'new';
}

function getOmitCodeAriaLabel(
accessibility: RenderCellArgs['accessibility'],
sideName: SideName
): string | undefined {
if (accessibility !== 'screen-reader') {
return undefined;
}
return sideName === 'old' ? 'Not in old version' : 'Not in new version';
}

function renderOmitCells(args: RenderCellArgs): Array<JSX.Element | false> {
const {hideGutter, gutterClassName, codeClassName, accessibility, side} = args;
const gutterClassNameValue = classNames('diff-gutter', 'diff-gutter-omit', gutterClassName);
const codeClassNameValue = classNames('diff-code', 'diff-code-omit', codeClassName);
const codeAriaLabel = getOmitCodeAriaLabel(accessibility, getSideName(side));

return [
!hideGutter && <td key="gutter" className={gutterClassNameValue} />,
<td key="code" className={codeClassNameValue} aria-label={codeAriaLabel} />,
];
}

function renderChangeCells(args: RenderCellArgs & {change: ChangeData}): Array<JSX.Element | false> {
const {
change,
side,
Expand All @@ -67,21 +97,12 @@ function renderCells(args: RenderCellArgs) {
hover,
renderToken,
renderGutter,
accessibility,
} = args;

if (!change) {
const gutterClassNameValue = classNames('diff-gutter', 'diff-gutter-omit', gutterClassName);
const codeClassNameValue = classNames('diff-code', 'diff-code-omit', codeClassName);

return [
!hideGutter && <td key="gutter" className={gutterClassNameValue} />,
<td key="code" className={codeClassNameValue} />,
];
}

const sideName = getSideName(side);
const {type, content} = change;
const changeKey = getChangeKey(change);
const sideName = side === SIDE_OLD ? 'old' : 'new';
const gutterClassNameValue = classNames(
'diff-gutter',
`diff-gutter-${type}`,
Expand All @@ -98,12 +119,16 @@ function renderCells(args: RenderCellArgs) {
renderDefault: renderDefaultBy(change, sideName),
wrapInAnchor: wrapInAnchorBy(gutterAnchor, gutterAnchorTarget),
};
const gutterAriaLabel
= accessibility === 'screen-reader' ? getGutterAriaLabel(change, sideName) : undefined;
const gutterProps = {
id: anchorID || undefined,
className: gutterClassNameValue,
children: renderGutter(gutterOptions),
...gutterEvents,
...(gutterAriaLabel && {'aria-label': gutterAriaLabel}),
};

const codeClassNameValue = classNames(
'diff-code',
`diff-code-${type}`,
Expand All @@ -113,6 +138,8 @@ function renderCells(args: RenderCellArgs) {
},
codeClassName
);
const codeAriaLabel
= accessibility === 'screen-reader' ? getCodeCellAriaLabel(change) : undefined;

return [
!hideGutter && <td key="gutter" {...gutterProps} data-change-key={changeKey} />,
Expand All @@ -123,20 +150,28 @@ function renderCells(args: RenderCellArgs) {
text={content}
tokens={tokens}
renderToken={renderToken}
screenReaderLabel={codeAriaLabel}
{...codeEvents}
/>,
];
}

function renderCells(args: RenderCellArgs): Array<JSX.Element | false> {
if (!args.change) {
return renderOmitCells(args);
}
return renderChangeCells(args as RenderCellArgs & {change: ChangeData});
}

interface SplitChangeProps extends ChangeSharedProps {
className: string;
oldChange: ChangeData | null;
newChange: ChangeData | null;
oldSelected: boolean;
newSelected: boolean;
oldTokens: TokenNode[] | null;
newTokens: TokenNode[] | null;
monotonous: boolean;
className: string;
oldChange: ChangeData | null;
newChange: ChangeData | null;
oldSelected: boolean;
newSelected: boolean;
oldTokens: TokenNode[] | null;
newTokens: TokenNode[] | null;
monotonous: boolean;
}

function SplitChange(props: SplitChangeProps) {
Expand All @@ -159,6 +194,7 @@ function SplitChange(props: SplitChangeProps) {
gutterAnchor,
renderToken,
renderGutter,
accessibility,
} = props;

const [hover, setHover] = useState('');
Expand All @@ -183,6 +219,7 @@ function SplitChange(props: SplitChangeProps) {
codeEvents,
renderToken,
renderGutter,
accessibility,
};
const oldArgs: RenderCellArgs = {
...commons,
Expand Down
19 changes: 15 additions & 4 deletions src/Hunk/UnifiedHunk/UnifiedChange.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {ChangeEventArgs, EventMap, GutterOptions, NativeEventMap, RenderGutter}
import {ChangeSharedProps} from '../interface';
import CodeCell from '../CodeCell';
import {composeCallback, renderDefaultBy, wrapInAnchorBy} from '../utils';
import {getGutterAriaLabel, getCodeCellAriaLabel} from '../accessibility';

interface UnifiedChangeProps extends ChangeSharedProps {
change: ChangeData;
Expand Down Expand Up @@ -44,7 +45,8 @@ function renderGutterCell(
anchorTarget: string | undefined,
events: NativeEventMap,
inHoverState: boolean,
renderGutter: RenderGutter
renderGutter: RenderGutter,
gutterAriaLabel?: string
) {
const gutterOptions: GutterOptions = {
change,
Expand All @@ -55,7 +57,12 @@ function renderGutterCell(
};

return (
<td className={className} {...events} data-change-key={changeKey}>
<td
className={className}
{...events}
data-change-key={changeKey}
{...(gutterAriaLabel && {'aria-label': gutterAriaLabel})}
>
{renderGutter(gutterOptions)}
</td>
);
Expand All @@ -77,6 +84,7 @@ function UnifiedChange(props: UnifiedChangeProps) {
generateAnchorID,
renderToken,
renderGutter,
accessibility,
} = props;
const {type, content} = change;
const changeKey = getChangeKey(change);
Expand Down Expand Up @@ -117,7 +125,8 @@ function UnifiedChange(props: UnifiedChangeProps) {
anchorID,
boundGutterEvents,
hover,
renderGutter
renderGutter,
accessibility === 'screen-reader' ? getGutterAriaLabel(change, 'old') : undefined
)
}
{
Expand All @@ -130,7 +139,8 @@ function UnifiedChange(props: UnifiedChangeProps) {
anchorID,
boundGutterEvents,
hover,
renderGutter
renderGutter,
accessibility === 'screen-reader' ? getGutterAriaLabel(change, 'new') : undefined
)
}
<CodeCell
Expand All @@ -139,6 +149,7 @@ function UnifiedChange(props: UnifiedChangeProps) {
text={content}
tokens={tokens}
renderToken={renderToken}
screenReaderLabel={accessibility === 'screen-reader' ? getCodeCellAriaLabel(change) : undefined}
{...boundCodeEvents}
/>
</tr>
Expand Down
44 changes: 44 additions & 0 deletions src/Hunk/accessibility.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import {
computeOldLineNumber,
computeNewLineNumber,
isDelete,
isInsert,
} from '../utils';
import type {ChangeData} from '../utils';
import type {Side} from '../interface';

export function getGutterAriaLabel(change: ChangeData, side: Side): string {
const oldNum = computeOldLineNumber(change);
const newNum = computeNewLineNumber(change);

if (side === 'old') {
if (oldNum === -1) {
return 'Not in old version';
}
if (isDelete(change)) {
return `Deleted line ${oldNum}`;
}
return `Old line ${oldNum}`;
}

if (newNum === -1) {
return 'Not in new version';
}
if (isInsert(change)) {
return `Added line ${newNum}`;
}
return `New line ${newNum}`;
}

export function getCodeCellAriaLabel(change: ChangeData): string {
const oldNum = computeOldLineNumber(change);
const newNum = computeNewLineNumber(change);

if (isInsert(change)) {
return `Added line ${newNum}`;
}
if (isDelete(change)) {
return `Deleted line ${oldNum}`;
}
return `Line ${newNum} unchanged`;
}
Loading