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

Add radio role and aria checked state to color checkboxes #2331

Open
wants to merge 21 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
4fc40b1
Add radio role and aria checked state to color checkboxes
RoyEJohnson Aug 20, 2024
8febcd7
Add radiogroup role to fieldset
RoyEJohnson Aug 20, 2024
d3bc546
Add trash can icon to color picker
RoyEJohnson Sep 9, 2024
fdf55a3
Only show trashcan when it will do something
RoyEJohnson Sep 12, 2024
1b7bf3b
Separate trash button; fix tests
RoyEJohnson Sep 18, 2024
510fd4a
Hide colorpicker legends; remove space instruction
RoyEJohnson Sep 23, 2024
f3b4f94
No fieldset grouping color indicators and trash icon
RoyEJohnson Sep 25, 2024
4b7b54c
Merge branch 'main' into make-color-picker-a-radio-group
staxly[bot] Oct 1, 2024
a44c8d5
Merge branch 'main' into make-color-picker-a-radio-group
staxly[bot] Oct 5, 2024
df42c4c
Merge branch 'main' into make-color-picker-a-radio-group
staxly[bot] Oct 10, 2024
d5d440a
Merge branch 'main' into make-color-picker-a-radio-group
staxly[bot] Oct 10, 2024
4ee9e26
Merge branch 'main' into make-color-picker-a-radio-group
staxly[bot] Oct 10, 2024
0616db0
Merge branch 'main' into make-color-picker-a-radio-group
staxly[bot] Oct 10, 2024
31e6b4f
Merge branch 'main' into make-color-picker-a-radio-group
staxly[bot] Oct 10, 2024
86c9fba
Merge branch 'main' into make-color-picker-a-radio-group
staxly[bot] Oct 10, 2024
09c0a6d
Merge branch 'main' into make-color-picker-a-radio-group
staxly[bot] Oct 10, 2024
bb19096
Merge branch 'main' into make-color-picker-a-radio-group
staxly[bot] Oct 10, 2024
d8da6f1
Merge branch 'main' into make-color-picker-a-radio-group
staxly[bot] Oct 10, 2024
c3f8784
Merge branch 'main' into make-color-picker-a-radio-group
staxly[bot] Oct 10, 2024
73b40eb
Merge branch 'main' into make-color-picker-a-radio-group
staxly[bot] Oct 15, 2024
31ea915
Merge branch 'main' into make-color-picker-a-radio-group
staxly[bot] Oct 31, 2024
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
29 changes: 29 additions & 0 deletions src/app/content/highlights/components/ColorIndicator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { Check } from 'styled-icons/fa-solid/Check';
import { isDefined } from '../../../guards';
import { highlightStyles } from '../../constants';
import { defaultFocusOutline } from '../../../theme';
import { useIntl } from 'react-intl';
import trashIcon from '../../../../assets/trash-347.svg';

interface StyleProps {
style: typeof highlightStyles[number];
Expand Down Expand Up @@ -114,4 +116,31 @@ const ColorIndicator = styled(Hoc)`
}
`;

function TB({
onClick,
className,
}: {
onClick: React.MouseEventHandler<HTMLButtonElement> | undefined;
className: string;
}) {
return (
<button
type='button'
className={className}
aria-label={useIntl().formatMessage({id: 'i18n:a11y:keyboard-shortcuts:deselect-highlight-color'})}
onClick={onClick}
>
<img src={trashIcon} alt='' />
</button>
);
}

// tslint:disable-next-line:variable-name
export const TrashButton = styled(TB)`
img {
height: ${(props: Props) => indicatorSize(props) - 0.5}rem;
width: ${(props: Props) => indicatorSize(props) - 0.5}rem;
}
`;

export default ColorIndicator;
23 changes: 22 additions & 1 deletion src/app/content/highlights/components/ColorPicker.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { renderToDom, dispatchKeyDownEvent } from '../../../../test/reactutils';
import TestContainer from '../../../../test/TestContainer';
import { highlightStyles } from '../../constants';
import ColorPicker from './ColorPicker';
import { HTMLElement, HTMLFieldSetElement, HTMLInputElement } from '@openstax/types/lib.dom';
import { HTMLElement, HTMLFieldSetElement, HTMLInputElement, HTMLButtonElement } from '@openstax/types/lib.dom';
import { assertDocument } from '../../../utils';

describe('ColorPicker', () => {
Expand Down Expand Up @@ -84,6 +84,27 @@ describe('ColorPicker', () => {
expect(onChange).not.toHaveBeenCalled();
});

it('calls remove when trashcan is clicked', () => {
const onChange = jest.fn();
const onRemove = jest.fn();

const {root} = renderToDom(<TestContainer>
<ColorPicker color={highlightStyles[0].label} onChange={onChange} onRemove={onRemove} />
</TestContainer>);

const button = root.querySelector('button') as HTMLButtonElement;

// This should trigger the button, but does not in testing
dispatchKeyDownEvent({
element: button as HTMLElement,
key: 'Enter',
});

button.click();
expect(onRemove).toHaveBeenCalled();
expect(onChange).not.toHaveBeenCalled();
});

it('operates as a radiogroup', () => {
const onChange = jest.fn();
const onRemove = jest.fn();
Expand Down
80 changes: 47 additions & 33 deletions src/app/content/highlights/components/ColorPicker.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { HighlightColorEnum } from '@openstax/highlighter/dist/api';
import React from 'react';
import { useIntl } from 'react-intl';
import { hiddenButAccessible } from '../../../theme';
import styled from 'styled-components/macro';
import { match, not } from '../../../fpUtils';
import { highlightStyles } from '../../constants';
import { cardPadding } from '../constants';
import ColorIndicator from './ColorIndicator';
import ColorIndicator, { TrashButton } from './ColorIndicator';
import { HTMLDivElement, HTMLInputElement } from '@openstax/types/lib.dom';

interface SingleSelectProps {
Expand Down Expand Up @@ -48,7 +49,7 @@ const ColorButton = styled(({className, size, style, ...props}: ColorButtonProps
component={<label />}
className={className}
>
<input type='checkbox' {...props} />
<input type='radio' aria-checked={props.checked} {...props} />
</ColorIndicator>;
})`
cursor: pointer;
Expand Down Expand Up @@ -82,6 +83,17 @@ function nextIdx(idx: number, itemCount: number, key: NavKeys) {
return idx;
}

// tslint:disable-next-line:variable-name
const FSWrapper = styled.div`
border: 0;
display: flex;
flex-direction: row;

legend {
${hiddenButAccessible}
}
`;

// tslint:disable-next-line:variable-name
const ColorPicker = ({className, ...props}: Props) => {
const ref = React.useRef<HTMLDivElement>(null);
Expand Down Expand Up @@ -113,33 +125,43 @@ const ColorPicker = ({className, ...props}: Props) => {
},
[color]
);
const hasOnRemove = 'onRemove' in props && props.onRemove;

React.useEffect(focusOnSelected, [focusOnSelected]);

return (
<fieldset
className={className}
tabIndex={0}
ref={ref}
onKeyDown={handleKeyNavigation}
onFocus={focusOnSelected}
>
<legend>Choose highlight color</legend>
{highlightStyles.map((style) => <ColorButton key={style.label}
name={style.label}
checked={props.multiple ? props.selected.includes(style.label) : props.color === style.label}
style={style}
size={props.size}
tabIndex={-1}
onChange={() => props.multiple
? props.selected.includes(style.label)
? props.onChange(props.selected.filter(not(match(style.label))))
: props.onChange([...props.selected, style.label])
: props.color === style.label
? props.onRemove ? props.onRemove() : null
: props.onChange(style.label)}
/>)}
</fieldset>
<FSWrapper>
<fieldset
className={className}
tabIndex={0}
ref={ref}
onKeyDown={handleKeyNavigation}
onFocus={focusOnSelected}
role='radiogroup'
>
<legend>Choose highlight color</legend>
{highlightStyles.map((style) => <ColorButton key={style.label}
name={style.label}
checked={props.multiple ? props.selected.includes(style.label) : props.color === style.label}
style={style}
size={props.size}
tabIndex={-1}
onChange={() => props.multiple
? props.selected.includes(style.label)
? props.onChange(props.selected.filter(not(match(style.label))))
: props.onChange([...props.selected, style.label])
: props.color === style.label
? props.onRemove ? props.onRemove() : null
: props.onChange(style.label)}
/>)}
</fieldset>
{ (!hasOnRemove || props.size === 'small') ? null :
<TrashButton
size={props.size}
onClick={props.onRemove}
/>
}
</FSWrapper>
);
};

Expand All @@ -151,12 +173,4 @@ export default styled(ColorPicker)`
overflow: visible;
gap: ${cardPadding}rem;
padding: ${cardPadding}rem 0.3rem;

legend {
position: absolute;
height: 1px;
width: 1px;
overflow: hidden;
clip: rect(1px, 1px, 1px, 1px);
}
`;
30 changes: 20 additions & 10 deletions src/app/content/highlights/components/EditCard.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,9 @@ describe('EditCard', () => {
const data = {
...highlightData,
annotation: '',
color: 'red',
};
const component = renderer.create(
const {root} = renderer.create(
<TestContainer services={services} store={store}>
<EditCard
{...editCardProps}
Expand All @@ -131,7 +132,8 @@ describe('EditCard', () => {
</TestContainer>
);

const picker = component.root.findByType(ColorPicker);
const picker = root.findByType(ColorPicker);

renderer.act(() => {
picker.props.onRemove();
});
Expand All @@ -154,14 +156,25 @@ describe('EditCard', () => {
);

const picker = component.root.findByType(ColorPicker);
renderer.act(() => {
picker.props.onRemove();
});

expect(editCardProps.onRemove).not.toHaveBeenCalled();
expect(picker.props.onRemove).toBeNull();
mockSpyUser.mockClear();
});

it('doesn\'t chain ColorPicker onRemove if there is no data', () => {
const mockSpyUser = jest.spyOn(selectAuth, 'user')
.mockReturnValue(formatUser(testAccountsUser));
const component = renderer.create(
<TestContainer services={services} store={store}>
<EditCard {...editCardProps} isActive={true} />
</TestContainer>
);

const picker = component.root.findByType(ColorPicker);

expect(picker.props.onRemove).toBeNull();
mockSpyUser.mockClear();
});
it('doesn\'t chain ColorPicker onRemove if there is a pending note', () => {
highlight.getStyle.mockReturnValue('red');
const mockSpyUser = jest.spyOn(selectAuth, 'user')
Expand All @@ -186,11 +199,8 @@ describe('EditCard', () => {
});

const picker = component.root.findByType(ColorPicker);
renderer.act(() => {
picker.props.onRemove();
});

expect(editCardProps.onRemove).not.toHaveBeenCalled();
expect(picker.props.onRemove).toBeNull();
mockSpyUser.mockClear();
});

Expand Down
14 changes: 9 additions & 5 deletions src/app/content/highlights/components/EditCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -251,13 +251,17 @@ function ActiveEditCard({
function useOnRemove(props: EditCardProps, pendingAnnotation: string) {
const onRemove = props.onRemove;
const trackDeleteHighlight = useAnalyticsEvent('deleteHighlight');
const removeAndTrack = React.useCallback(
() => {
const data = assertDefined(props.data, 'props.data must be defined');

return React.useCallback(() => {
if (props.data && !props.data.annotation && !pendingAnnotation) {
onRemove();
trackDeleteHighlight(props.data.color);
}
}, [onRemove, pendingAnnotation, props.data, trackDeleteHighlight]);
trackDeleteHighlight(data.color);
},
[onRemove, props.data, trackDeleteHighlight]
);

return props.data && !props.data.annotation && !pendingAnnotation && props.data.color ? removeAndTrack : null;
}

function AnnotationEditor({
Expand Down
Loading
Loading