* [891615] Keyboard focus is lost or misplaced due to user interaction or content update
* [892423] Keyboard focus is lost or misplaced due to user interaction or content update
'issue.type.VULNERABILITY',
'issue.type.CODE_SMELL',
'issue.type.SECURITY_HOTSPOT'
- ].forEach(name => expect(screen.getByRole('link', { name })).toBeInTheDocument());
+ ].forEach(name => expect(screen.getByRole('button', { name })).toBeInTheDocument());
});
});
renderCodingRulesApp(mockLoggedInUser());
const selectQP = handler.allQualityProfile('js')[0];
- await user.click(await screen.findByRole('link', { name: 'JavaScript' }));
+ await user.click(await screen.findByRole('button', { name: 'JavaScript' }));
await user.click(await screen.findByRole('button', { name: 'bulk_change' }));
await user.click(await screen.findByRole('link', { name: 'coding_rules.activate_in…' }));
const dialog = screen.getByRole('dialog', {
handler.setIsAdmin();
renderCodingRulesApp(mockLoggedInUser());
- await user.click(await screen.findByRole('link', { name: 'C' }));
+ await user.click(await screen.findByRole('button', { name: 'C' }));
await user.click(await screen.findByRole('button', { name: 'bulk_change' }));
await user.click(await screen.findByRole('link', { name: 'coding_rules.activate_in…' }));
const dialog = screen.getByRole('dialog', {
margin-right: -4px;
}
+button.search-navigator-facet {
+ text-align: start;
+}
+
.search-navigator-facet .leak-box {
height: var(--controlHeight);
line-height: var(--controlHeight);
it('should support OWASP Top 10 version 2021', async () => {
const user = userEvent.setup();
renderIssueApp();
- await user.click(await screen.findByRole('link', { name: 'issues.facet.standards' }));
- const owaspTop102021 = screen.getByRole('link', { name: 'issues.facet.owaspTop10_2021' });
+ await user.click(await screen.findByRole('button', { name: 'issues.facet.standards' }));
+ const owaspTop102021 = screen.getByRole('button', { name: 'issues.facet.owaspTop10_2021' });
expect(owaspTop102021).toBeInTheDocument();
await user.click(owaspTop102021);
const standard = await handler.getStandards();
/* eslint-disable-next-line testing-library/render-result-naming-convention */
const linkName = renderOwaspTop102021Category(standard, val);
- expect(await screen.findByRole('link', { name: linkName })).toBeInTheDocument();
+ expect(await screen.findByRole('button', { name: linkName })).toBeInTheDocument();
})
);
});
);
expect(
- await screen.findByRole('link', { name: `issue.type.${IssueType.CodeSmell}` })
+ await screen.findByRole('button', { name: `issue.type.${IssueType.CodeSmell}` })
).toBeInTheDocument();
});
});
);
}
- handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
+ getUrlOptionForSingleValue = (option: string) => {
+ return this.isSelected(option) ? null : option;
+ };
+
+ getUrlOptionForMultiValue = (
+ event: React.MouseEvent<HTMLButtonElement>,
+ option: string,
+ value: Option[]
+ ) => {
+ if (event.ctrlKey || event.metaKey) {
+ if (this.isSelected(option)) {
+ return value.length > 1 ? value.filter(val => val !== option).join(',') : null;
+ }
+
+ return value.concat(option).join(',');
+ }
+
+ return this.isSelected(option) && value.length < 2 ? null : option;
+ };
+
+ handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
- event.currentTarget.blur();
const { property, value } = this.props;
const { key: option } = event.currentTarget.dataset;
- let urlOption;
if (option) {
- if (Array.isArray(value) && (event.ctrlKey || event.metaKey)) {
- if (this.isSelected(option)) {
- urlOption = value.length > 1 ? value.filter(val => val !== option).join(',') : null;
- } else {
- urlOption = value.concat(option).join(',');
- }
- } else {
- urlOption =
- this.isSelected(option) && (!Array.isArray(value) || value.length < 2) ? null : option;
- }
+ const urlOption = Array.isArray(value)
+ ? this.getUrlOptionForMultiValue(event, option, value)
+ : this.getUrlOptionForSingleValue(option);
this.props.onQueryChange({ [property]: urlOption });
}
'facet',
'search-navigator-facet',
'projects-facet',
+ 'button-link',
{
active: this.isSelected(option),
'search-navigator-facet-half': this.props.halfWidth
option > value;
return (
- <a
+ <button
aria-label={this.props.renderAccessibleLabel(option)}
className={className}
data-key={option}
- href="#"
+ type="button"
+ tabIndex={0}
key={option}
onClick={this.handleClick}>
<span className="facet-name">
{this.renderOptionBar(facetValue)}
</span>
)}
- </a>
+ </button>
);
}
*/
import { shallow } from 'enzyme';
import * as React from 'react';
+import { mockEvent } from '../../../../helpers/testUtils';
import Filter from '../Filter';
it('renders', () => {
).toMatchSnapshot();
});
+it('should handle click when value is single', () => {
+ const onQueryChange = jest.fn();
+ const wrapper = shallowRender({ onQueryChange, value: 'option1' });
+
+ // select
+ wrapper.instance().handleClick(mockEvent({ currentTarget: { dataset: { key: 'option2' } } }));
+ expect(onQueryChange).toBeCalledWith({ foo: 'option2' });
+
+ onQueryChange.mockClear();
+
+ // deselect
+ wrapper.instance().handleClick(mockEvent({ currentTarget: { dataset: { key: 'option1' } } }));
+ expect(onQueryChange).toBeCalledWith({ foo: null });
+});
+
+it('should handle click when value is array', () => {
+ const onQueryChange = jest.fn();
+ const wrapper = shallowRender({ onQueryChange, value: ['option1', 'option2'] });
+
+ // select one
+ wrapper.instance().handleClick(mockEvent({ currentTarget: { dataset: { key: 'option2' } } }));
+ expect(onQueryChange).toBeCalledWith({ foo: 'option2' });
+
+ onQueryChange.mockClear();
+
+ // select other
+ wrapper.instance().handleClick(mockEvent({ currentTarget: { dataset: { key: 'option3' } } }));
+ expect(onQueryChange).toBeCalledWith({ foo: 'option3' });
+
+ onQueryChange.mockClear();
+
+ // select additional
+ wrapper
+ .instance()
+ .handleClick(mockEvent({ ctrlKey: true, currentTarget: { dataset: { key: 'option3' } } }));
+ expect(onQueryChange).toBeCalledWith({ foo: 'option1,option2,option3' });
+
+ onQueryChange.mockClear();
+
+ // deselect one
+ wrapper
+ .instance()
+ .handleClick(mockEvent({ metaKey: true, currentTarget: { dataset: { key: 'option2' } } }));
+ expect(onQueryChange).toBeCalledWith({ foo: 'option1' });
+});
+
+it('should handle click when value is array with one value', () => {
+ const onQueryChange = jest.fn();
+ const wrapper = shallowRender({ onQueryChange, value: ['option1'] });
+
+ // deselect one
+ wrapper
+ .instance()
+ .handleClick(mockEvent({ ctrlKey: true, currentTarget: { dataset: { key: 'option1' } } }));
+ expect(onQueryChange).toBeCalledWith({ foo: null });
+
+ onQueryChange.mockClear();
+
+ // deselect one
+ wrapper.instance().handleClick(mockEvent({ currentTarget: { dataset: { key: 'option1' } } }));
+ expect(onQueryChange).toBeCalledWith({ foo: null });
+});
+
function shallowRender(overrides: Partial<Filter['props']> = {}) {
return shallow<Filter>(
<Filter
<div
className="search-navigator-facet-list projects-facet-list"
>
- <a
+ <button
aria-label="1"
- className="facet search-navigator-facet projects-facet"
+ className="facet search-navigator-facet projects-facet button-link"
data-key={1}
- href="#"
key="1"
onClick={[Function]}
+ tabIndex={0}
+ type="button"
>
<span
className="facet-name"
>
1
</span>
- </a>
+ </button>
<div
className="search-navigator-facet-highlight-under-container"
>
- <a
+ <button
aria-label="2"
- className="facet search-navigator-facet projects-facet"
+ className="facet search-navigator-facet projects-facet button-link"
data-key={2}
- href="#"
key="2"
onClick={[Function]}
+ tabIndex={0}
+ type="button"
>
<span
className="facet-name"
>
2
</span>
- </a>
- <a
+ </button>
+ <button
aria-label="3"
- className="facet search-navigator-facet projects-facet"
+ className="facet search-navigator-facet projects-facet button-link"
data-key={3}
- href="#"
key="3"
onClick={[Function]}
+ tabIndex={0}
+ type="button"
>
<span
className="facet-name"
>
3
</span>
- </a>
+ </button>
</div>
</div>
</div>
<div
className="search-navigator-facet-list projects-facet-list"
>
- <a
+ <button
aria-label="1"
- className="facet search-navigator-facet projects-facet"
+ className="facet search-navigator-facet projects-facet button-link"
data-key={1}
- href="#"
key="1"
onClick={[Function]}
+ tabIndex={0}
+ type="button"
>
<span
className="facet-name"
>
1
</span>
- </a>
+ </button>
<div
className="search-navigator-facet-highlight-under-container"
>
- <a
+ <button
aria-label="2"
- className="facet search-navigator-facet projects-facet active"
+ className="facet search-navigator-facet projects-facet button-link active"
data-key={2}
- href="#"
key="2"
onClick={[Function]}
+ tabIndex={0}
+ type="button"
>
<span
className="facet-name"
>
2
</span>
- </a>
- <a
+ </button>
+ <button
aria-label="3"
- className="facet search-navigator-facet projects-facet"
+ className="facet search-navigator-facet projects-facet button-link"
data-key={3}
- href="#"
key="3"
onClick={[Function]}
+ tabIndex={0}
+ type="button"
>
<span
className="facet-name"
>
3
</span>
- </a>
+ </button>
</div>
</div>
</div>
<div
className="search-navigator-facet-list projects-facet-list"
>
- <a
+ <button
aria-label="1"
- className="facet search-navigator-facet projects-facet"
+ className="facet search-navigator-facet projects-facet button-link"
data-key={1}
- href="#"
key="1"
onClick={[Function]}
+ tabIndex={0}
+ type="button"
>
<span
className="facet-name"
>
1
</span>
- </a>
- <a
+ </button>
+ <button
aria-label="2"
- className="facet search-navigator-facet projects-facet"
+ className="facet search-navigator-facet projects-facet button-link"
data-key={2}
- href="#"
key="2"
onClick={[Function]}
+ tabIndex={0}
+ type="button"
>
<span
className="facet-name"
>
2
</span>
- </a>
- <a
+ </button>
+ <button
aria-label="3"
- className="facet search-navigator-facet projects-facet"
+ className="facet search-navigator-facet projects-facet button-link"
data-key={3}
- href="#"
key="3"
onClick={[Function]}
+ tabIndex={0}
+ type="button"
>
<span
className="facet-name"
>
3
</span>
- </a>
+ </button>
</div>
</div>
`;
<div
className="search-navigator-facet-list projects-facet-list"
>
- <a
+ <button
aria-label="a"
- className="facet search-navigator-facet projects-facet"
+ className="facet search-navigator-facet projects-facet button-link"
data-key="a"
- href="#"
key="a"
onClick={[Function]}
+ tabIndex={0}
+ type="button"
>
<span
className="facet-name"
/>
</div>
</span>
- </a>
- <a
+ </button>
+ <button
aria-label="b"
- className="facet search-navigator-facet projects-facet"
+ className="facet search-navigator-facet projects-facet button-link"
data-key="b"
- href="#"
key="b"
onClick={[Function]}
+ tabIndex={0}
+ type="button"
>
<span
className="facet-name"
/>
</div>
</span>
- </a>
- <a
+ </button>
+ <button
aria-label="c"
- className="facet search-navigator-facet projects-facet"
+ className="facet search-navigator-facet projects-facet button-link"
data-key="c"
- href="#"
key="c"
onClick={[Function]}
+ tabIndex={0}
+ type="button"
>
<span
className="facet-name"
/>
</div>
</span>
- </a>
+ </button>
</div>
</div>
`;
<div
className="search-navigator-facet-list projects-facet-list"
>
- <a
+ <button
aria-label="1"
- className="facet search-navigator-facet projects-facet"
+ className="facet search-navigator-facet projects-facet button-link"
data-key={1}
- href="#"
key="1"
onClick={[Function]}
+ tabIndex={0}
+ type="button"
>
<span
className="facet-name"
>
1
</span>
- </a>
- <a
+ </button>
+ <button
aria-label="2"
- className="facet search-navigator-facet projects-facet"
+ className="facet search-navigator-facet projects-facet button-link"
data-key={2}
- href="#"
key="2"
onClick={[Function]}
+ tabIndex={0}
+ type="button"
>
<span
className="facet-name"
>
2
</span>
- </a>
- <a
+ </button>
+ <button
aria-label="3"
- className="facet search-navigator-facet projects-facet"
+ className="facet search-navigator-facet projects-facet button-link"
data-key={3}
- href="#"
key="3"
onClick={[Function]}
+ tabIndex={0}
+ type="button"
>
<span
className="facet-name"
>
3
</span>
- </a>
+ </button>
</div>
<footer />
</div>
<div
className="search-navigator-facet-list projects-facet-list"
>
- <a
+ <button
aria-label="1"
- className="facet search-navigator-facet projects-facet active"
+ className="facet search-navigator-facet projects-facet button-link active"
data-key={1}
- href="#"
key="1"
onClick={[Function]}
+ tabIndex={0}
+ type="button"
>
<span
className="facet-name"
>
1
</span>
- </a>
- <a
+ </button>
+ <button
aria-label="2"
- className="facet search-navigator-facet projects-facet active"
+ className="facet search-navigator-facet projects-facet button-link active"
data-key={2}
- href="#"
key="2"
onClick={[Function]}
+ tabIndex={0}
+ type="button"
>
<span
className="facet-name"
>
2
</span>
- </a>
- <a
+ </button>
+ <button
aria-label="3"
- className="facet search-navigator-facet projects-facet"
+ className="facet search-navigator-facet projects-facet button-link"
data-key={3}
- href="#"
key="3"
onClick={[Function]}
+ tabIndex={0}
+ type="button"
>
<span
className="facet-name"
>
3
</span>
- </a>
+ </button>
</div>
</div>
`;
<div
className="search-navigator-facet-list projects-facet-list"
>
- <a
+ <button
aria-label="1"
- className="facet search-navigator-facet projects-facet"
+ className="facet search-navigator-facet projects-facet button-link"
data-key={1}
- href="#"
key="1"
onClick={[Function]}
+ tabIndex={0}
+ type="button"
>
<span
className="facet-name"
>
1
</span>
- </a>
- <a
+ </button>
+ <button
aria-label="2"
- className="facet search-navigator-facet projects-facet active"
+ className="facet search-navigator-facet projects-facet button-link active"
data-key={2}
- href="#"
key="2"
onClick={[Function]}
+ tabIndex={0}
+ type="button"
>
<span
className="facet-name"
>
2
</span>
- </a>
- <a
+ </button>
+ <button
aria-label="3"
- className="facet search-navigator-facet projects-facet"
+ className="facet search-navigator-facet projects-facet button-link"
data-key={3}
- href="#"
key="3"
onClick={[Function]}
+ tabIndex={0}
+ type="button"
>
<span
className="facet-name"
>
3
</span>
- </a>
+ </button>
</div>
</div>
`;
<div
className="search-navigator-facet-list projects-facet-list"
>
- <a
+ <button
aria-label="projects.facets.label_text_x.projects.facets.languages.Java"
- className="facet search-navigator-facet projects-facet active"
+ className="facet search-navigator-facet projects-facet button-link active"
data-key="java"
- href="#"
key="java"
onClick={[Function]}
+ tabIndex={0}
+ type="button"
>
<span
className="facet-name"
>
39
</span>
- </a>
- <a
+ </button>
+ <button
aria-label="projects.facets.label_text_x.projects.facets.languages.C#"
- className="facet search-navigator-facet projects-facet active"
+ className="facet search-navigator-facet projects-facet button-link active"
data-key="cs"
- href="#"
key="cs"
onClick={[Function]}
+ tabIndex={0}
+ type="button"
>
<span
className="facet-name"
>
4
</span>
- </a>
- <a
+ </button>
+ <button
aria-label="projects.facets.label_text_x.projects.facets.languages.JavaScript"
- className="facet search-navigator-facet projects-facet"
+ className="facet search-navigator-facet projects-facet button-link"
data-key="js"
- href="#"
key="js"
onClick={[Function]}
+ tabIndex={0}
+ type="button"
>
<span
className="facet-name"
>
1
</span>
- </a>
+ </button>
</div>
<SearchableFilterFooter
onQueryChange={[MockFunction]}
<div
className="search-navigator-facet-list projects-facet-list"
>
- <a
+ <button
aria-label="projects.facets.label_text_x.projects.facets.tags.lang"
- className="facet search-navigator-facet projects-facet active"
+ className="facet search-navigator-facet projects-facet button-link active"
data-key="lang"
- href="#"
key="lang"
onClick={[Function]}
+ tabIndex={0}
+ type="button"
>
<span
className="facet-name"
>
4
</span>
- </a>
- <a
+ </button>
+ <button
aria-label="projects.facets.label_text_x.projects.facets.tags.sonar"
- className="facet search-navigator-facet projects-facet active"
+ className="facet search-navigator-facet projects-facet button-link active"
data-key="sonar"
- href="#"
key="sonar"
onClick={[Function]}
+ tabIndex={0}
+ type="button"
>
<span
className="facet-name"
>
3
</span>
- </a>
- <a
+ </button>
+ <button
aria-label="projects.facets.label_text_x.projects.facets.tags.csharp"
- className="facet search-navigator-facet projects-facet"
+ className="facet search-navigator-facet projects-facet button-link"
data-key="csharp"
- href="#"
key="csharp"
onClick={[Function]}
+ tabIndex={0}
+ type="button"
>
<span
className="facet-name"
>
1
</span>
- </a>
+ </button>
</div>
<SearchableFilterFooter
isLoading={false}
}
export default class FacetHeader extends React.PureComponent<Props> {
- handleClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
+ handleClick = (event: React.SyntheticEvent<HTMLButtonElement>) => {
event.preventDefault();
event.nativeEvent.preventDefault();
if (this.props.onClick) {
<div className="search-navigator-facet-header-wrapper display-flex-center">
{this.props.onClick ? (
<span className="search-navigator-facet-header display-flex-center">
- <a href="#" onClick={this.handleClick} aria-expanded={this.props.open}>
+ <button
+ className="button-link"
+ type="button"
+ onClick={this.handleClick}
+ aria-expanded={this.props.open}
+ tabIndex={0}>
<OpenCloseIcon className="little-spacer-right" open={this.props.open} />
{this.props.name}
- </a>
+ </button>
{this.renderHelper()}
</span>
) : (
loading: false
};
- handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
+ handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
- event.currentTarget.blur();
this.props.onClick(this.props.value, event.ctrlKey || event.metaKey);
};
render() {
const { name } = this.props;
- const className = classNames('search-navigator-facet', this.props.className, {
+ const className = classNames('search-navigator-facet button-link', this.props.className, {
active: this.props.active,
'search-navigator-facet-half': this.props.halfWidth
});
{this.renderValue()}
</span>
) : (
- <a
+ <button
className={className}
data-facet={this.props.value}
- href="#"
onClick={this.handleClick}
- title={this.props.tooltip}>
+ tabIndex={0}
+ title={this.props.tooltip}
+ type="button">
<span className="facet-name">{name}</span>
{this.renderValue()}
- </a>
+ </button>
);
}
}
renderFacet(undefined, undefined, undefined, { onClick: onFacetClick });
// Start closed.
- let facetHeader = screen.getByRole('link', { name: 'foo', expanded: false });
+ let facetHeader = screen.getByRole('button', { name: 'foo', expanded: false });
expect(facetHeader).toBeInTheDocument();
expect(screen.queryByText('Foo/Bar')).not.toBeInTheDocument();
// Expand.
facetHeader.click();
- facetHeader = screen.getByRole('link', { name: 'foo', expanded: true });
+ facetHeader = screen.getByRole('button', { name: 'foo', expanded: true });
expect(facetHeader).toBeInTheDocument();
expect(screen.getByText('Foo/Bar')).toBeInTheDocument();
// Interact with facets.
- const facet1 = screen.getByRole('link', { name: 'Foo/Bar 10' });
+ const facet1 = screen.getByRole('button', { name: 'Foo/Bar 10' });
expect(facet1).toHaveClass('active');
facet1.click();
expect(onFacetClick).toBeCalledWith('bar', false);
- const facet2 = screen.getByRole('link', { name: 'Foo/Baz' });
+ const facet2 = screen.getByRole('button', { name: 'Foo/Baz' });
expect(facet2).not.toHaveClass('active');
- expect(screen.queryByRole('link', { name: 'Foo/Bat' })).not.toBeInTheDocument();
+ expect(screen.queryByRole('button', { name: 'Foo/Bat' })).not.toBeInTheDocument();
expect(screen.getByText('Foo/Bat')).toBeInTheDocument();
// Collapse again.
facetHeader.click();
- expect(screen.getByRole('link', { name: 'foo', expanded: false })).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: 'foo', expanded: false })).toBeInTheDocument();
expect(screen.queryByText('Foo/Bar')).not.toBeInTheDocument();
});
it('should correctly render a disabled header', () => {
renderFacet(undefined, { onClick: undefined });
- expect(screen.queryByRole('link', { name: 'foo' })).not.toBeInTheDocument();
+ expect(screen.queryByRole('button', { name: 'foo' })).not.toBeInTheDocument();
});
it('should correctly render a facet item list with title', () => {
transition: none;
}
-a.search-navigator-facet {
+button.search-navigator-facet {
opacity: 1;
cursor: pointer;
}
-a.search-navigator-facet .facet-name {
+button.search-navigator-facet .facet-name {
color: var(--baseFontColor);
}
-a.search-navigator-facet:hover,
-a.search-navigator-facet:focus,
+button.search-navigator-facet:hover,
+button.search-navigator-facet:focus,
.search-navigator-facet.active {
border-color: var(--blue);
}
.search-navigator-facet-header {
display: block;
flex-shrink: 0;
- padding: 8px 0;
+ padding: 8px 1px;
color: var(--baseFontColor);
font-weight: 600;
overflow: hidden;
white-space: nowrap;
}
-.search-navigator-facet-header > a {
+.search-navigator-facet-header > button {
border-bottom: none;
color: var(--baseFontColor);
}
-.search-navigator-facet-header > a:focus,
-.search-navigator-facet-header > a:hover {
+.search-navigator-facet-header > button:focus,
+.search-navigator-facet-header > button:hover {
color: var(--darkBlue);
}