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

Merged
merged 25 commits into from
Feb 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
9ccb2be
Add radio role and aria checked state to color checkboxes
RoyEJohnson Aug 20, 2024
839c115
Add radiogroup role to fieldset
RoyEJohnson Aug 20, 2024
aeab5c9
Add trash can icon to color picker
RoyEJohnson Sep 9, 2024
d6754b0
Only show trashcan when it will do something
RoyEJohnson Sep 12, 2024
8021937
Separate trash button; fix tests
RoyEJohnson Sep 18, 2024
3ae7822
Hide colorpicker legends; remove space instruction
RoyEJohnson Sep 23, 2024
7e72b37
No fieldset grouping color indicators and trash icon
RoyEJohnson Sep 25, 2024
3481c58
Merge branch 'main' into make-color-picker-a-radio-group
staxly[bot] Jan 23, 2025
7333339
Merge branch 'main' into make-color-picker-a-radio-group
staxly[bot] Feb 5, 2025
2fd9432
Merge branch 'main' into make-color-picker-a-radio-group
staxly[bot] Feb 5, 2025
0722f8e
Merge branch 'main' into make-color-picker-a-radio-group
staxly[bot] Feb 5, 2025
13ba934
Merge branch 'main' into make-color-picker-a-radio-group
staxly[bot] Feb 5, 2025
796c87d
Merge branch 'main' into make-color-picker-a-radio-group
staxly[bot] Feb 5, 2025
8cfdb14
Merge branch 'main' into make-color-picker-a-radio-group
staxly[bot] Feb 5, 2025
af8184e
Merge branch 'main' into make-color-picker-a-radio-group
staxly[bot] Feb 5, 2025
1aef8d9
Merge branch 'main' into make-color-picker-a-radio-group
staxly[bot] Feb 5, 2025
1866ad3
Merge branch 'main' into make-color-picker-a-radio-group
staxly[bot] Feb 5, 2025
a018e8f
Merge branch 'main' into make-color-picker-a-radio-group
staxly[bot] Feb 5, 2025
0f4e2ad
Merge branch 'main' into make-color-picker-a-radio-group
staxly[bot] Feb 5, 2025
df0cacd
Merge branch 'main' into make-color-picker-a-radio-group
staxly[bot] Feb 5, 2025
a09aa38
Merge branch 'main' into make-color-picker-a-radio-group
staxly[bot] Feb 5, 2025
24e00e2
Merge branch 'main' into make-color-picker-a-radio-group
staxly[bot] Feb 11, 2025
e6c6af2
Merge branch 'main' into make-color-picker-a-radio-group
staxly[bot] Feb 11, 2025
066dc05
Merge branch 'main' into make-color-picker-a-radio-group
staxly[bot] Feb 12, 2025
8747f71
Merge branch 'main' into make-color-picker-a-radio-group
staxly[bot] Feb 13, 2025
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