diff --git a/docs/accessibility.md b/docs/accessibility.md index 774283b8b78..f2ba2ec386d 100644 --- a/docs/accessibility.md +++ b/docs/accessibility.md @@ -54,7 +54,7 @@ For example: ``` -Known reactstrap components that accept the `color` prop and work with custom Treeherder colors: `Badge`, `Button`, `Card`, `Dropdown.Toggle`, `FormText`, `NavBar`, `Progress`, `Spinner`. +Known reactstrap components that accept the `color` prop and work with custom Treeherder colors: `Badge`, `Button`, `Card`, `DropdownToggle`, `FormText`, `NavBar`, `Progress`, `Spinner`. In case you need to add more custom colors, please add on [treeherder-custom-styles.css](https://github.com/mozilla/treeherder/blob/master/ui/css/treeherder-custom-styles.css#L348) style sheet. @@ -102,7 +102,7 @@ If your case is more specific, please check [this guide](https://css-tricks.com/ ## Interactive elements -When creating elements that have event listeners, prefer any component of the reactstrap interactive elements. Examples are: `Button`, `Input`, `Dropdown.Toggle`. You can also choose a HTML `` element. +When creating elements that have event listeners, prefer any component of the reactstrap interactive elements. Examples are: `Button`, `Input`, `DropdownToggle`. You can also choose a HTML `` element. If you need to insert an event listener in a non-interactive element, such as a `span`, add also an `aria-role` of `button`, `link`, `checkbox`, or whatever seems closer to the functionality of the element. @@ -116,10 +116,10 @@ If you need to insert an event listener in a non-interactive element, such as a There is a special case when you are creating a dropdown menu. First of all, try to follow [reactstrap structure](https://reactstrap.github.io/components/dropdowns/). -Lastly, insert an additional tag `prop` to `Dropdown.Item` component. +Lastly, insert an additional tag `prop` to `DropdownItem` component. ```jsx - Menu Item + Menu Item ``` ## Forms, inputs and buttons diff --git a/package.json b/package.json index d9119551da3..4bcedcf6072 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,6 @@ "prop-types": "15.8.1", "query-string": "7.0.1", "react": "18.3.1", - "react-bootstrap": "2.10.10", "react-dates": "21.8.0", "react-dom": "18.3.1", "react-helmet": "6.1.0", @@ -56,6 +55,7 @@ "react-split-pane": "0.1.92", "react-table-6": "6.11.0", "react-tabs": "6.1.0", + "reactstrap": "8.10.1", "redoc": "2.4.0", "redux": "4.2.1", "redux-debounce": "1.0.1", diff --git a/tests/ui/job-view/Filtering_test.jsx b/tests/ui/job-view/Filtering_test.jsx index fb3069c6eea..1d09dbc4cce 100644 --- a/tests/ui/job-view/Filtering_test.jsx +++ b/tests/ui/job-view/Filtering_test.jsx @@ -198,11 +198,6 @@ describe('Filtering', () => { ); expect(unfilteredPushes).toHaveLength(10); - // Open the filters dropdown to reveal menu items - const filtersDropdown = await waitFor(() => getByTitle('Set filters')); - fireEvent.click(filtersDropdown); - - // Wait for dropdown to open and find "My pushes only" const myPushes = await waitFor(() => getByText('My pushes only')); fireEvent.click(myPushes); diff --git a/tests/ui/perfherder/alerts-view/alerts_test.jsx b/tests/ui/perfherder/alerts-view/alerts_test.jsx index bb6604472ec..19bede40e3a 100644 --- a/tests/ui/perfherder/alerts-view/alerts_test.jsx +++ b/tests/ui/perfherder/alerts-view/alerts_test.jsx @@ -622,20 +622,8 @@ test('Selecting `all` from (frameworks|projects) dropdown shows all (frameworks| const { queryAllByText, getByTestId } = alertsView(); const allFromDropdown = await waitFor(() => queryAllByText(/all/)); - // Find the actual clickable parent elements (dropdown items) that contain the text "all" - const clickableElements = allFromDropdown - .map( - (textNode) => - textNode.closest('a') || - textNode.closest('button') || - textNode.closest('[role="button"]'), - ) - .filter(Boolean); - - if (clickableElements.length >= 2) { - fireEvent.click(clickableElements[0]); - fireEvent.click(clickableElements[1]); - } + fireEvent.click(allFromDropdown[0]); + fireEvent.click(allFromDropdown[1]); const alert1 = await waitFor(() => getByTestId('69526')); const alert2 = await waitFor(() => getByTestId('69530')); @@ -752,7 +740,7 @@ test(`table data cannot be sorted by 'Tags & Options'`, async () => { getAllByTitle('Sorted by tags & options disabled'), ); - expect(sortByTags[0]).toBeDisabled(); + expect(sortByTags[0]).toHaveClass('disabled-button'); }); test(`table data can be sorted in ascending order by 'Confidence'`, async () => { diff --git a/tests/ui/perfherder/alerts-view/modal_perf_tags_test.jsx b/tests/ui/perfherder/alerts-view/modal_perf_tags_test.jsx index 93042df1e5e..9ebc12d6e8e 100644 --- a/tests/ui/perfherder/alerts-view/modal_perf_tags_test.jsx +++ b/tests/ui/perfherder/alerts-view/modal_perf_tags_test.jsx @@ -54,9 +54,9 @@ test('An active/checked tag can be unchecked', async () => { test('Modal closes on X', async () => { const handleClose = jest.fn(); - const { getByLabelText } = testTagsModal(handleClose); + const { getByText } = testTagsModal(handleClose); - const closeButton = await waitFor(() => getByLabelText('Close')); + const closeButton = await waitFor(() => getByText('×')); expect(closeButton).toBeInTheDocument(); @@ -69,7 +69,7 @@ test('Modal does not keep unsaved changes', async () => { testAlertSummary.performance_tags = ['harness']; const handleClose = jest.fn(); - const { getByLabelText, getByTestId } = testTagsModal(handleClose); + const { getByText, getByTestId } = testTagsModal(handleClose); let activeTag = await waitFor(() => getByTestId('modal-perf-tag harness')); @@ -78,7 +78,7 @@ test('Modal does not keep unsaved changes', async () => { fireEvent.change(activeTag, { target: { checked: false } }); expect(activeTag.checked).toBeFalsy(); - const closeButton = await waitFor(() => getByLabelText('Close')); + const closeButton = await waitFor(() => getByText('×')); fireEvent.click(closeButton); expect(handleClose).toHaveBeenCalledTimes(1); diff --git a/tests/ui/perfherder/alerts-view/select_alert_framework_test.jsx b/tests/ui/perfherder/alerts-view/select_alert_framework_test.jsx index b52fd7ac126..f47e1ac0dcf 100644 --- a/tests/ui/perfherder/alerts-view/select_alert_framework_test.jsx +++ b/tests/ui/perfherder/alerts-view/select_alert_framework_test.jsx @@ -1,4 +1,4 @@ -import { render, fireEvent } from '@testing-library/react'; +import { render } from '@testing-library/react'; import { noop } from 'lodash'; import React from 'react'; @@ -41,14 +41,10 @@ const testFrameworksDropdown = () => { }; test('should pin the right number of items to top and bottom frameworks w.r.t config', async () => { - const { container } = testFrameworksDropdown(); + testFrameworksDropdown(); // top pinned represents all items in the drop down that is at the top of the list // bottom pinned represents all items in the drop down that is at the bottom of the list. - // Open the dropdown to render the menu items - const dropdownToggle = container.querySelector('.dropdown-toggle'); - fireEvent.click(dropdownToggle); - const topPinned = document.querySelectorAll('.top-pinned'); const bottomPinned = document.querySelectorAll('.bottom-pinned'); diff --git a/tests/ui/perfherder/alerts-view/status_dropdown_test.jsx b/tests/ui/perfherder/alerts-view/status_dropdown_test.jsx index 224c4dd7c4e..c6efafa7765 100644 --- a/tests/ui/perfherder/alerts-view/status_dropdown_test.jsx +++ b/tests/ui/perfherder/alerts-view/status_dropdown_test.jsx @@ -53,10 +53,6 @@ afterEach(cleanup); test("Summary with no tags shows 'Add tags'", async () => { const { getByText } = testStatusDropdown([]); - // Open the status dropdown first - const statusDropdown = await waitFor(() => getByText('untriaged')); - fireEvent.click(statusDropdown); - const dropdownItem = await waitFor(() => getByText('Add tags')); expect(dropdownItem).toBeInTheDocument(); @@ -65,10 +61,6 @@ test("Summary with no tags shows 'Add tags'", async () => { test("Summary with tags shows 'Edit tags'", async () => { const { getByText } = testStatusDropdown(['harness']); - // Open the status dropdown first - const statusDropdown = await waitFor(() => getByText('untriaged')); - fireEvent.click(statusDropdown); - const dropdownItem = await waitFor(() => getByText('Edit tags')); expect(dropdownItem).toBeInTheDocument(); @@ -77,10 +69,6 @@ test("Summary with tags shows 'Edit tags'", async () => { test("Tags modal opens from 'Add tags'", async () => { const { getByText, getByTestId } = testStatusDropdown([]); - // Open the status dropdown first - const statusDropdown = await waitFor(() => getByText('untriaged')); - fireEvent.click(statusDropdown); - const dropdownItem = await waitFor(() => getByText('Add tags')); fireEvent.click(dropdownItem); @@ -93,10 +81,6 @@ test("Tags modal opens from 'Add tags'", async () => { test("Tags modal opens from 'Edit tags'", async () => { const { getByText, getByTestId } = testStatusDropdown(['harness']); - // Open the status dropdown first - const statusDropdown = await waitFor(() => getByText('untriaged')); - fireEvent.click(statusDropdown); - const dropdownItem = await waitFor(() => getByText('Edit tags')); fireEvent.click(dropdownItem); diff --git a/tests/ui/perfherder/compare-view/compare_table_test.jsx b/tests/ui/perfherder/compare-view/compare_table_test.jsx index 3d01ca18cf3..c4f22527e1e 100644 --- a/tests/ui/perfherder/compare-view/compare_table_test.jsx +++ b/tests/ui/perfherder/compare-view/compare_table_test.jsx @@ -361,10 +361,14 @@ test('page parameter updates with value 2 when clicking on the second page', asy expect(result2).toBeInTheDocument(); expect(result10).toBeInTheDocument(); - const secondPage = await waitFor(() => findAllByLabelText('Go to page 2')); + const secondPage = await waitFor(() => + findAllByLabelText('pagination-button-2'), + ); fireEvent.click(secondPage[0]); - const firstPage = await waitFor(() => findAllByLabelText('Go to page 1')); + const firstPage = await waitFor(() => + findAllByLabelText('pagination-button-1'), + ); expect(firstPage[0]).toBeInTheDocument(); expect(mockUpdateParams).toHaveBeenLastCalledWith({ page: 2 }); }); diff --git a/tests/ui/perfherder/dropdown_menu_test.jsx b/tests/ui/perfherder/dropdown_menu_test.jsx index e2547c5f152..5d848f2574b 100644 --- a/tests/ui/perfherder/dropdown_menu_test.jsx +++ b/tests/ui/perfherder/dropdown_menu_test.jsx @@ -27,8 +27,8 @@ const repoDropdownMenuItems = () => ); test('Pinned options are listed first', async () => { - const { container } = repoDropdownMenuItems(); - const items = container.querySelectorAll('.dropdown-item'); + const { getAllByRole } = repoDropdownMenuItems(); + const items = getAllByRole('menuitem', { hidden: true }); expect(items[0].textContent).toContain('fenix'); expect(items[1].textContent).toContain('autoland'); @@ -36,15 +36,15 @@ test('Pinned options are listed first', async () => { }); test('Bogus pinned items are not listed', async () => { - const { container } = repoDropdownMenuItems(); - const items = container.querySelectorAll('.dropdown-item'); + const { getAllByRole } = repoDropdownMenuItems(); + const items = getAllByRole('menuitem', { hidden: true }); expect(items[2].textContent).not.toContain('tunafish'); }); test('Unpinned items are sorted alphabetically', async () => { - const { container } = repoDropdownMenuItems(); - const items = container.querySelectorAll('.dropdown-item'); + const { getAllByRole } = repoDropdownMenuItems(); + const items = getAllByRole('menuitem', { hidden: true }); expect(items[3].textContent).toContain('ash'); expect(items[4].textContent).toContain('mozilla-central'); @@ -53,8 +53,8 @@ test('Unpinned items are sorted alphabetically', async () => { }); test('Pinned options are listed only once', async () => { - const { container } = repoDropdownMenuItems(); - const items = container.querySelectorAll('.dropdown-item'); + const { getAllByRole } = repoDropdownMenuItems(); + const items = getAllByRole('menuitem', { hidden: true }); expect(items[4].textContent).not.toContain('autoland'); expect(items[5].textContent).not.toContain('fenix'); diff --git a/tests/ui/perfherder/graphs-view/graphs_view_test.jsx b/tests/ui/perfherder/graphs-view/graphs_view_test.jsx index 110f40b25be..f1461b5ad0e 100644 --- a/tests/ui/perfherder/graphs-view/graphs_view_test.jsx +++ b/tests/ui/perfherder/graphs-view/graphs_view_test.jsx @@ -118,54 +118,14 @@ const graphsViewControls = ( afterEach(cleanup); test('Changing the platform dropdown in the Test Data Modal displays expected tests', async () => { - const { - getByText, - getByTitle, - getByTestId, - queryByText, - container, - } = graphsViewControls(); + const { getByText, getByTitle, getByTestId } = graphsViewControls(); fireEvent.click(getByText('Add test data')); - // Wait for the modal to fully load with platforms - const platform = await waitFor(() => getByTitle('Platform')); - - // The Platform title might be on the dropdown div, find the actual button - const platformButton = - platform.querySelector('button') || - platform.querySelector('.dropdown-toggle'); - if (platformButton) { - fireEvent.click(platformButton); - } else { - fireEvent.click(platform); - } - - // Small delay to allow dropdown animation - await new Promise((resolve) => { - setTimeout(resolve, 100); - }); - - // Try to find windows7-32 in the dropdown - let windowsPlatform = queryByText('windows7-32'); + const platform = getByTitle('Platform'); + fireEvent.click(platform); - // If not found, try clicking the dropdown again (sometimes it needs a second click) - if (!windowsPlatform) { - const dropdownButton = container.querySelector( - '[title="Platform"] button.dropdown-toggle', - ); - if (dropdownButton) { - fireEvent.click(dropdownButton); - await new Promise((resolve) => { - setTimeout(resolve, 100); - }); - } - } - - // Now wait for the platform option to appear - windowsPlatform = await waitFor(() => getByText('windows7-32'), { - timeout: 3000, - }); + const windowsPlatform = await waitFor(() => getByText('windows7-32')); fireEvent.click(windowsPlatform); // 'mozilla-central windows7-32 a11yr opt e10s stylo' @@ -359,8 +319,6 @@ test('Changing the platform dropdown while filtered by text in the Test Data Mod getByPlaceholderText, getByTitle, getByTestId, - queryByText, - container, } = graphsViewControls(); fireEvent.click(getByText('Add test data')); @@ -382,43 +340,10 @@ test('Changing the platform dropdown while filtered by text in the Test Data Mod expect(presentTests.children).toHaveLength(1); expect(linuxTest).toBeInTheDocument(); - const platform = await waitFor(() => getByTitle('Platform')); - - // The Platform title might be on the dropdown div, find the actual button - const platformButton = - platform.querySelector('button') || - platform.querySelector('.dropdown-toggle'); - if (platformButton) { - fireEvent.click(platformButton); - } else { - fireEvent.click(platform); - } - - // Small delay to allow dropdown animation - await new Promise((resolve) => { - setTimeout(resolve, 100); - }); - - // Try to find windows7-32 in the dropdown - let windowsPlatform = queryByText('windows7-32'); + const platform = getByTitle('Platform'); + fireEvent.click(platform); - // If not found, try clicking the dropdown again (sometimes it needs a second click) - if (!windowsPlatform) { - const dropdownButton = container.querySelector( - '[title="Platform"] button.dropdown-toggle', - ); - if (dropdownButton) { - fireEvent.click(dropdownButton); - await new Promise((resolve) => { - setTimeout(resolve, 100); - }); - } - } - - // Now wait for the platform option to appear - windowsPlatform = await waitFor(() => getByText('windows7-32'), { - timeout: 3000, - }); + const windowsPlatform = await waitFor(() => getByText('windows7-32')); fireEvent.click(windowsPlatform); // linux64 (default platform of the modal) and windows7-32 (the platform below) diff --git a/ui/css/perf.css b/ui/css/perf.css index 4b5106424ca..7f1ad3d59af 100644 --- a/ui/css/perf.css +++ b/ui/css/perf.css @@ -697,8 +697,3 @@ li.pagination-active.active > button { .multiline-text { white-space: pre-line; } - -.pagination .page-item.active .page-link { - background-color: #17a2b8; - border-color: #17a2b8; -} diff --git a/ui/infra-compare/InfraCompareTable.jsx b/ui/infra-compare/InfraCompareTable.jsx index bd45e52652b..e738572aa53 100644 --- a/ui/infra-compare/InfraCompareTable.jsx +++ b/ui/infra-compare/InfraCompareTable.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Table } from 'react-bootstrap'; +import { Table } from 'reactstrap'; import PropTypes from 'prop-types'; import { getJobsUrl } from '../helpers/url'; @@ -23,7 +23,7 @@ export default class InfraCompareTable extends React.PureComponent { sz="small" className="compare-table mb-0 px-0" key={platform} - ref={(el) => { + innerRef={(el) => { this.header = el; }} > diff --git a/ui/infra-compare/InfraCompareTableControls.jsx b/ui/infra-compare/InfraCompareTableControls.jsx index aab81f9decd..0a04e6bc2f8 100644 --- a/ui/infra-compare/InfraCompareTableControls.jsx +++ b/ui/infra-compare/InfraCompareTableControls.jsx @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Container, Form } from 'react-bootstrap'; +import { Container, CustomInput, Label } from 'reactstrap'; import FilterControls from '../shared/FilterControls'; @@ -110,11 +110,11 @@ export default class CompareTableControls extends React.Component { updateFilter={this.updateFilter} updateFilterText={this.updateFilterText} /> - - Filter percentage: {filterPercent}% - - Filter percentage: {filterPercent}% + + {suite} - + {originalValue} + + - {originalJobs.size > 0 ? ( - Array.from(originalJobs).map(([jobName, durations]) => ( -

- {jobName}: {durations.join(', ')} -

- )) - ) : ( -

No jobs to show

- )} -
- Go to treeherder Job View - - - } - delay={{ show: 0, hide: 0 }} + target={`originalValue${hashkey}`} + autohide={false} > - - {originalValue} - - + {originalJobs.size > 0 ? ( + Array.from(originalJobs).map(([jobName, durations]) => ( +

+ {jobName}: {durations.join(', ')} +

+ )) + ) : ( +

No jobs to show

+ )} + + Go to treeherder Job View + + {originalValue < newValue && <} {originalValue > newValue && >} - - {newJobs.size > 0 ? ( - Array.from(newJobs).map(([jobName, duration]) => ( -

- {jobName}: {duration.join(', ')} -

- )) - ) : ( -

No jobs to show

- )} - - Go to treeherder Job View - - - } - delay={{ show: 0, hide: 0 }} + - - {newValue} - -
+ {newValue} + + + {newJobs.size > 0 ? ( + Array.from(newJobs).map(([jobName, duration]) => ( +

+ {jobName}: {duration.join(', ')} +

+ )) + ) : ( +

No jobs to show

+ )} + + Go to treeherder Job View + +
{originalFailures} {originalFailures < newFailures && <} diff --git a/ui/infra-compare/InfraCompareTableView.jsx b/ui/infra-compare/InfraCompareTableView.jsx index c409c7526f5..bfb9b6358ca 100644 --- a/ui/infra-compare/InfraCompareTableView.jsx +++ b/ui/infra-compare/InfraCompareTableView.jsx @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Col, Row, Container, Alert } from 'react-bootstrap'; +import { Col, Row, Container, Alert } from 'reactstrap'; import ErrorMessages from '../shared/ErrorMessages'; import { genericErrorMessage, errorMessageClass } from '../helpers/constants'; @@ -211,7 +211,7 @@ export default class InfraCompareTableView extends React.Component { {jobsNotDisplayed && jobsNotDisplayed.length > 0 && ( - + { + this.setState((prevState) => ({ dropdownOpen: !prevState.dropdownOpen })); + }; + updateDateRange = (dateRange) => { this.setState({ dateRange }); if (dateRange === 'custom range') { @@ -37,7 +42,7 @@ export default class DateOptions extends React.Component { render() { const { updateState } = this.props; - const { dateRange } = this.state; + const { dropdownOpen, dateRange } = this.state; const dateOptions = [ 'last 7 days', 'last 30 days', @@ -47,13 +52,18 @@ export default class DateOptions extends React.Component { return (
- + + date range - + {dateRange === 'custom range' && ( )} diff --git a/ui/intermittent-failures/DateRangePicker.jsx b/ui/intermittent-failures/DateRangePicker.jsx index 4c6ba12f371..44824d97631 100644 --- a/ui/intermittent-failures/DateRangePicker.jsx +++ b/ui/intermittent-failures/DateRangePicker.jsx @@ -5,7 +5,7 @@ import { DateRangePickerPhrases } from 'react-dates/lib/defaultPhrases'; import { DateRangePicker as DatePickerAirbnb } from 'react-dates'; import moment from 'moment'; import PropTypes from 'prop-types'; -import { Button } from 'react-bootstrap'; +import { Button } from 'reactstrap'; import { ISODate } from './helpers'; @@ -91,7 +91,7 @@ export default class DateRangePicker extends React.Component { isOutsideRange={(day) => moment().diff(day) < 0} phrases={defaultPhrases} /> -
diff --git a/ui/intermittent-failures/Graph.jsx b/ui/intermittent-failures/Graph.jsx index 5838e432404..d66f3012c2b 100644 --- a/ui/intermittent-failures/Graph.jsx +++ b/ui/intermittent-failures/Graph.jsx @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { VictoryChart, VictoryLine, VictoryLegend } from 'victory'; -import { Col } from 'react-bootstrap'; +import { Col } from 'reactstrap'; const Graph = ({ graphData, title, legendData }) => (