Skip to content
Merged
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
240 changes: 239 additions & 1 deletion localization/TranscriberAdmin-en-1.2.xliff

Large diffs are not rendered by default.

204 changes: 204 additions & 0 deletions localization/TranscriberAdmin-en.xlf

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,15 @@ const createInitialState = (
const mockSharedStrings = new LocalizedStrings({
en: {
wait: 'Please wait',
close: 'Close',
},
});

const mockWorkflowStepsStrings = new LocalizedStrings({
en: {
record: 'Record',
review: 'Review',
recordTip: 'Record tip',
},
});

Expand Down Expand Up @@ -106,28 +108,38 @@ const createPassageDetailState = (
currentstep: 'step-1',
recording: false,
commentRecording: false,
stepComplete: () => false,
setCurrentStep: cy.stub(),
...overrides,
}) as ICtxState;

const mountMobileWorkflowSteps = ({
currentstep = 'step-1',
workflow,
completedStepIds = [],
remoteBusy = false,
recording = false,
commentRecording = false,
}: {
currentstep?: string;
workflow?: ICtxState['workflow'];
completedStepIds?: string[];
remoteBusy?: boolean;
recording?: boolean;
commentRecording?: boolean;
} = {}) => {
const setCurrentStep = cy.stub().as('setCurrentStep');
const ctxState = createPassageDetailState({
const ctxOverrides: Partial<ICtxState> = {
currentstep,
recording,
commentRecording,
setCurrentStep,
});
stepComplete: (id: string) => completedStepIds.includes(id),
};
if (workflow) {
ctxOverrides.workflow = workflow;
}
const ctxState = createPassageDetailState(ctxOverrides);
const initialState = createInitialState({ remoteBusy });

cy.mount(
Expand All @@ -153,6 +165,38 @@ describe('MobileWorkflowSteps', () => {
cy.get('[data-cy="workflow-step-label"]').should('contain.text', 'Record');
});

it('shows current, complete, and incomplete step colors', () => {
mountMobileWorkflowSteps({
workflow: [
{ id: 'step-1', label: 'Record' },
{ id: 'step-2', label: 'Review' },
{ id: 'step-3', label: 'Publish' },
],
completedStepIds: ['step-2'],
});

cy.get('[data-cy="workflow-step"]')
.eq(0)
.should('have.css', 'background-color', 'rgb(97, 97, 97)');
cy.get('[data-cy="workflow-step"]')
.eq(1)
.should('have.css', 'background-color', 'rgb(189, 189, 189)');
cy.get('[data-cy="workflow-step"]')
.eq(2)
.should('have.css', 'background-color', 'rgb(238, 238, 238)');
});

it('shows a tip dialog for the current step', () => {
mountMobileWorkflowSteps();

cy.get('[data-cy="workflow-step-tip"]').click();

cy.get('[role="dialog"]').should('be.visible');
cy.contains('Record tip').should('be.visible');
cy.contains('Close').click();
cy.get('[role="dialog"]').should('not.exist');
});

it('selects a different step when clicked', () => {
mountMobileWorkflowSteps();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,44 @@
import { Box, Typography, useTheme } from '@mui/material';
import {
Box,
Button,
ButtonBase,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Typography,
useTheme,
} from '@mui/material';
import InfoIcon from '@mui/icons-material/Info';
import usePassageDetailContext from '../../../context/usePassageDetailContext';
import { useGetGlobal } from '../../../context/useGlobal';
import { useSnackBar } from '../../../hoc/SnackBar';
import { sharedSelector } from '../../../selector';
import { sharedSelector, workflowStepsSelector } from '../../../selector';
import { shallowEqual, useSelector } from 'react-redux';
import { useWfLabel } from '../../../utils/useWfLabel';
import { useMemo, useState } from 'react';
import { IWorkflowStepsStrings } from '../../../model';
import { toCamel } from '../../../utils/toCamel';

export default function MobileWorkflowSteps() {
const { workflow, currentstep, setCurrentStep, recording, commentRecording } =
usePassageDetailContext();
const {
workflow,
currentstep,
setCurrentStep,
recording,
commentRecording,
stepComplete,
} = usePassageDetailContext();
const getGlobal = useGetGlobal();
const { showMessage } = useSnackBar();
const ts = useSelector(sharedSelector, shallowEqual);
const theme = useTheme();
const getWfLabel = useWfLabel();
const t: IWorkflowStepsStrings = useSelector(
workflowStepsSelector,
shallowEqual
);
const [tipOpen, setTipOpen] = useState(false);

const handleSelect = (id: string) => () => {
if (getGlobal('remoteBusy')) {
Expand All @@ -25,6 +50,19 @@ export default function MobileWorkflowSteps() {
}
};

const currentLabel = useMemo(() => {
return workflow.find((w) => w.id === currentstep)?.label ?? '';
}, [currentstep, workflow]);
const currentTip = useMemo(() => {
if (!currentLabel) {
return '';
}
const tipKey = toCamel(currentLabel + 'Tip');
return Object.prototype.hasOwnProperty.call(t, tipKey)
? t.getString(tipKey)
: '';
}, [currentLabel, t]);

return (
<Box sx={{ px: 1.5, py: 0.5 }} data-cy="workflow-steps">
<Box
Expand All @@ -39,43 +77,63 @@ export default function MobileWorkflowSteps() {
{workflow.map((step) => {
const isCurrent = step.id === currentstep;
return (
<Box
<ButtonBase
key={step.id}
data-cy="workflow-step"
role="button"
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The step indicator buttons are focusable/clickable but have no accessible name (no text, no aria-label). Add an aria-label (e.g., derived from getWfLabel(step.label)) so screen readers can announce what each step button represents.

Suggested change
role="button"
role="button"
aria-label={getWfLabel(step.label)}

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is way more information given to the user of the mobile app so I don't think this is appropriate.

onClick={handleSelect(step.id)}
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
}
}}
sx={{
width: 36,
height: 14,
backgroundColor: isCurrent
? theme.palette.grey[600]
: theme.palette.grey[300],
? theme.palette.grey[700]
: stepComplete(step.id)
? theme.palette.grey[400]
: theme.palette.grey[200],
transform: 'skewX(-20deg)',
borderRadius: '2px',
cursor:
recording || commentRecording ? 'not-allowed' : 'pointer',
flexShrink: 0,
}}
>
<Box
sx={{
width: '100%',
height: '100%',
transform: 'skewX(20deg)',
}}
/>
</Box>
/>
);
})}
</Box>
{workflow.find((w) => w.id === currentstep)?.label && (
{currentLabel && (
<Typography
variant="caption"
sx={{ display: 'block', textAlign: 'center' }}
data-cy="workflow-step-label"
>
{getWfLabel(workflow.find((w) => w.id === currentstep)?.label ?? '')}
{currentTip ? (
<ButtonBase
onClick={() => setTipOpen(true)}
data-cy="workflow-step-tip"
sx={{ borderRadius: 1 }}
aria-label={currentTip}
>
{getWfLabel(currentLabel) + '\u00A0'}
<InfoIcon color={'info'} fontSize="small" />
</ButtonBase>
) : (
getWfLabel(currentLabel)
)}
</Typography>
)}
<Dialog open={tipOpen} onClose={() => setTipOpen(false)}>
<DialogTitle>{getWfLabel(currentLabel)}</DialogTitle>
<DialogContent>{currentTip}</DialogContent>
<DialogActions>
<Button onClick={() => setTipOpen(false)}>{ts.close}</Button>
</DialogActions>
</Dialog>
</Box>
);
}
Loading