From b2f4d2412a27c0f0fe2a359d65f2d3e1c56276da Mon Sep 17 00:00:00 2001 From: prv-proton Date: Thu, 2 Jan 2025 02:15:27 -0800 Subject: [PATCH] Feat: LCFS - Improve Accessibility for AG Grid Filtering Components #1290 --- .../components/Filters/BCColumnSetFilter.jsx | 4 +- .../Filters/BCDateFloatingFilter.jsx | 122 +++++++++++++++++ .../Filters/BCSelectFloatingFilter.jsx | 128 ++++++++++++++++++ .../components/BCDataGrid/components/index.js | 2 + frontend/src/themes/base/globals.js | 30 ++++ .../Admin/AdminMenu/components/Users.jsx | 2 +- .../Admin/AdminMenu/components/_schema.js | 36 ++--- .../ComplianceReports/components/_schema.jsx | 20 +-- .../Organizations/ViewOrganization/_schema.js | 12 +- frontend/src/views/Transactions/_schema.js | 11 +- 10 files changed, 326 insertions(+), 41 deletions(-) create mode 100644 frontend/src/components/BCDataGrid/components/Filters/BCDateFloatingFilter.jsx create mode 100644 frontend/src/components/BCDataGrid/components/Filters/BCSelectFloatingFilter.jsx diff --git a/frontend/src/components/BCDataGrid/components/Filters/BCColumnSetFilter.jsx b/frontend/src/components/BCDataGrid/components/Filters/BCColumnSetFilter.jsx index 2bc840392..a95e6bf96 100644 --- a/frontend/src/components/BCDataGrid/components/Filters/BCColumnSetFilter.jsx +++ b/frontend/src/components/BCDataGrid/components/Filters/BCColumnSetFilter.jsx @@ -6,7 +6,9 @@ import CheckBoxIcon from '@mui/icons-material/CheckBox' const icon = const checkedIcon = - +/** + * @deprecated + */ export const BCColumnSetFilter = forwardRef((props, ref) => { const { apiQuery, params } = props const [options, setOptions] = useState([]) diff --git a/frontend/src/components/BCDataGrid/components/Filters/BCDateFloatingFilter.jsx b/frontend/src/components/BCDataGrid/components/Filters/BCDateFloatingFilter.jsx new file mode 100644 index 000000000..18c77d81e --- /dev/null +++ b/frontend/src/components/BCDataGrid/components/Filters/BCDateFloatingFilter.jsx @@ -0,0 +1,122 @@ +import { useState, useEffect, useCallback } from 'react' +import { FormControl, IconButton, InputAdornment } from '@mui/material' +import { + Clear as ClearIcon, + CalendarToday as CalendarIcon +} from '@mui/icons-material' +import { DatePicker } from '@mui/x-date-pickers' +import { format, isValid } from 'date-fns' +import { getDateFromDateSections } from '@mui/x-date-pickers/internals/hooks/useField/useField.utils' + +export const BCDateFloatingFilter = ({ + model, + onModelChange, + disabled = false, + initialFilterType = 'equals', + label = 'Select Date' +}) => { + const [selectedDate, setSelectedDate] = useState(null) + const [open, setOpen] = useState(false) + + const handleChange = useCallback((newDate) => { + setSelectedDate(newDate) + + if (newDate && isValid(newDate)) { + onModelChange({ + type: initialFilterType, + dateFrom: format(newDate, 'yyyy-MM-dd'), + dateTo: null, + filterType: 'date' + }) + } else { + onModelChange(null) + } + }, []) + + const handleClear = (event) => { + event.stopPropagation() + setSelectedDate(null) + onModelChange(null) + } + + const handleOpen = () => { + setOpen(true) + } + + const handleClose = () => { + setOpen(false) + } + + useEffect(() => { + if (!model) { + setSelectedDate(null) + return + } + + if (model.filter) { + const date = new Date(model.dateFrom) + setSelectedDate(isValid(date) ? date : null) + } + }, [model]) + + return ( + + + setOpen(true)} + > + + + + ), + endAdornment: selectedDate && ( + + event.stopPropagation()} + edge="end" + > + + + + ) + } + } + }} + /> + + ) +} + +BCDateFloatingFilter.displayName = 'BCDateFloatingFilter' diff --git a/frontend/src/components/BCDataGrid/components/Filters/BCSelectFloatingFilter.jsx b/frontend/src/components/BCDataGrid/components/Filters/BCSelectFloatingFilter.jsx new file mode 100644 index 000000000..c0c37168f --- /dev/null +++ b/frontend/src/components/BCDataGrid/components/Filters/BCSelectFloatingFilter.jsx @@ -0,0 +1,128 @@ +import { useState, useCallback, useEffect } from 'react' +import { IconButton } from '@mui/material' +import { Clear as ClearIcon } from '@mui/icons-material' +const ITEM_HEIGHT = 48 +const ITEM_PADDING_TOP = 8 + +export const BCSelectFloatingFilter = ({ + model, + onModelChange, + optionsQuery, + valueKey = 'value', + labelKey = 'label', + disabled = false, + params, + initialFilterType = 'equals', + multiple = false, + initialSelectedValues = [] +}) => { + const [selectedValues, setSelectedValues] = useState([]) + const { data: optionsData, isLoading, isError, error } = optionsQuery(params) + + const handleChange = (event) => { + const { options } = event.target + const newValues = Array.from(options) + .filter((option) => option.selected) + .map((option) => option.value) + + if (!multiple) { + setSelectedValues([newValues[0] || '']) + onModelChange( + !newValues[0] || newValues[0] === '0' + ? null + : { + type: initialFilterType, + filter: newValues[0] + } + ) + } else { + setSelectedValues(newValues) + onModelChange({ + type: initialFilterType, + filter: newValues + }) + } + } + + const handleClear = (event) => { + event.stopPropagation() + setSelectedValues([]) + onModelChange(null) + } + + const renderSelectContent = useCallback(() => { + if (isLoading) { + return ( + + ) + } + + if (isError) { + return ( + + ) + } + + return (optionsData || []).map((option) => ( + + )) + }, [isLoading, isError, optionsData, error]) + + useEffect(() => { + if (!model) { + setSelectedValues(initialSelectedValues) + } else { + setSelectedValues([model?.filter]) + } + }, [model, initialSelectedValues]) + + return ( +
+
+ + {selectedValues.length > 0 && ( + event.stopPropagation()} + aria-label="Clear selection" + > + + + )} +
+
+ ) +} + +BCSelectFloatingFilter.displayName = 'BCSelectFloatingFilter' diff --git a/frontend/src/components/BCDataGrid/components/index.js b/frontend/src/components/BCDataGrid/components/index.js index 14d4ca2be..16e855ef3 100644 --- a/frontend/src/components/BCDataGrid/components/index.js +++ b/frontend/src/components/BCDataGrid/components/index.js @@ -13,3 +13,5 @@ export { BCPagination } from './StatusBar/BCPagination' export { LargeTextareaEditor } from './Editors/LargeTextareaEditor' export { TextCellEditor } from './Editors/TextCellEditor' export { NumberEditor } from './Editors/NumberEditor' +export { BCDateFloatingFilter } from './Filters/BCDateFloatingFilter' +export { BCSelectFloatingFilter } from './Filters/BCSelectFloatingFilter' diff --git a/frontend/src/themes/base/globals.js b/frontend/src/themes/base/globals.js index f8a092509..bb9d0f51b 100644 --- a/frontend/src/themes/base/globals.js +++ b/frontend/src/themes/base/globals.js @@ -114,6 +114,36 @@ const globals = { fontWeight: 700, color: grey[700] }, + '.select-container': { + fontFamily: "'BCSans', 'Noto Sans', 'Verdana', 'Arial', 'sans-serif'", + fontSize: '1.6rem', + width: '100%', + display: 'flex', + alignItems: 'center', + gap: '8px', + border: 'none', + borderBottom: '2px solid #495057', + borderRadius: '0px', + padding: '0px', + background: '#fff', + transition: 'border-color 0.3s ease-in-out, box-shadow 0.3s ease-in-out' + }, + '.select-container:focus-within': { + borderColor: '2px solid #495057', + border: '0.01rem solid #495057', + }, + '.select-container #select-filter': { + width: '100%', + padding: '11px', + border: 'none', + outline: 'none', + appearance: 'none', + background: 'transparent' + }, + '.select-container option': { + fontSize: '1rem', + fontFamily: 'inherit' + }, // editor theme for ag-grid quertz theme '.ag-theme-quartz': { '--ag-borders': `0.5px solid ${grey[400]} !important`, diff --git a/frontend/src/views/Admin/AdminMenu/components/Users.jsx b/frontend/src/views/Admin/AdminMenu/components/Users.jsx index 28f2e021b..49921701b 100644 --- a/frontend/src/views/Admin/AdminMenu/components/Users.jsx +++ b/frontend/src/views/Admin/AdminMenu/components/Users.jsx @@ -43,7 +43,7 @@ export const Users = () => { navigate(ROUTES.ADMIN_USERS_ADD) } const getRowId = useCallback((params) => { - return params.data.userProfileId + return params.data.userProfileId.toString() }, []) const handleRowClicked = useCallback((params) => { diff --git a/frontend/src/views/Admin/AdminMenu/components/_schema.js b/frontend/src/views/Admin/AdminMenu/components/_schema.js index 70b652b71..058803361 100644 --- a/frontend/src/views/Admin/AdminMenu/components/_schema.js +++ b/frontend/src/views/Admin/AdminMenu/components/_schema.js @@ -8,8 +8,11 @@ import { RoleRenderer, StatusRenderer } from '@/utils/grid/cellRenderers' -import { BCColumnSetFilter } from '@/components/BCDataGrid/components' import { useRoleList } from '@/hooks/useRole' +import { + BCSelectFloatingFilter, + BCDateFloatingFilter +} from '@/components/BCDataGrid/components/index' export const usersColumnDefs = (t) => [ { @@ -44,14 +47,12 @@ export const usersColumnDefs = (t) => [ }, suppressFilterButton: true }, - floatingFilterComponent: BCColumnSetFilter, + floatingFilterComponent: BCSelectFloatingFilter, floatingFilterComponentParams: { - apiQuery: useRoleList, // all data returned should be an array which includes an object of key 'name' - // Eg: [{id: 1, name: 'EntryListItem' }] except name all others are optional + optionsQuery: useRoleList, params: 'government_roles_only=true', - key: 'admin-users', - disableCloseOnSelect: false, - multiple: false + valueKey: 'name', + labelKey: 'name' }, cellRenderer: RoleRenderer, cellClass: 'vertical-middle' @@ -84,17 +85,17 @@ export const usersColumnDefs = (t) => [ }, cellRenderer: StatusRenderer, cellClass: 'vertical-middle', - floatingFilterComponent: BCColumnSetFilter, + floatingFilterComponent: BCSelectFloatingFilter, floatingFilterComponentParams: { - apiQuery: () => ({ + optionsQuery: () => ({ data: [ { id: 1, name: t('admin:userColLabels.active') }, { id: 0, name: t('admin:userColLabels.inactive') } ], isLoading: false }), - disableCloseOnSelect: false, - multiple: false + valueKey: 'name', + labelKey: 'name' }, minWidth: 120, suppressHeaderMenuButton: false @@ -169,6 +170,13 @@ export const userLoginHistoryColDefs = (t) => [ buttons: ['clear'] } }, + { + field: 'createDate', + headerName: t('admin:userLoginHistoryColLabels.createDate'), + cellDataType: 'dateString', + valueFormatter: timezoneFormatter, + floatingFilterComponent: BCDateFloatingFilter + }, { field: 'keycloakEmail', headerName: t('admin:userLoginHistoryColLabels.keycloakEmail'), @@ -193,12 +201,6 @@ export const userLoginHistoryColDefs = (t) => [ field: 'loginErrorMessage', headerName: t('admin:userLoginHistoryColLabels.loginErrorMessage'), cellDataType: 'string' - }, - { - field: 'createDate', - headerName: t('admin:userLoginHistoryColLabels.createDate'), - cellDataType: 'dateString', - valueFormatter: timezoneFormatter } ] diff --git a/frontend/src/views/ComplianceReports/components/_schema.jsx b/frontend/src/views/ComplianceReports/components/_schema.jsx index e7f1546fb..6c52b88b6 100644 --- a/frontend/src/views/ComplianceReports/components/_schema.jsx +++ b/frontend/src/views/ComplianceReports/components/_schema.jsx @@ -1,4 +1,7 @@ -import { BCColumnSetFilter } from '@/components/BCDataGrid/components' +import { + BCDateFloatingFilter, + BCSelectFloatingFilter +} from '@/components/BCDataGrid/components' import { SUMMARY } from '@/constants/common' import { ReportsStatusRenderer } from '@/utils/grid/cellRenderers' import { timezoneFormatter } from '@/utils/formatters' @@ -50,10 +53,10 @@ export const reportsColDefs = (t, bceidRole) => [ url: ({ data }) => `${data.compliancePeriod?.description}/${data.complianceReportId}` }, - floatingFilterComponent: BCColumnSetFilter, + floatingFilterComponent: BCSelectFloatingFilter, floatingFilterComponentParams: { // TODO: change this to api Query later - apiQuery: () => ({ + optionsQuery: () => ({ data: bceidRole ? [ { id: 1, name: 'Draft' }, @@ -70,17 +73,15 @@ export const reportsColDefs = (t, bceidRole) => [ ], isLoading: false }), - key: 'report-status', - label: t('report:reportColLabels.status'), - disableCloseOnSelect: false, - multiple: false + valueKey: 'name', + labelKey: 'name' } }, { field: 'updateDate', cellDataType: 'dateString', headerName: t('report:reportColLabels.lastUpdated'), - flex: 1, + minWidth: '80', valueGetter: ({ data }) => data.updateDate || '', valueFormatter: timezoneFormatter, filter: 'agDateColumnFilter', @@ -89,7 +90,8 @@ export const reportsColDefs = (t, bceidRole) => [ suppressAndOrCondition: true, buttons: ['clear'], maxValidYear: 2400 - } + }, + floatingFilterComponent: BCDateFloatingFilter } ] diff --git a/frontend/src/views/Organizations/ViewOrganization/_schema.js b/frontend/src/views/Organizations/ViewOrganization/_schema.js index bc1a33fb8..0d1363f7f 100644 --- a/frontend/src/views/Organizations/ViewOrganization/_schema.js +++ b/frontend/src/views/Organizations/ViewOrganization/_schema.js @@ -1,6 +1,6 @@ import { numberFormatter } from '@/utils/formatters' import { LinkRenderer, OrgStatusRenderer } from '@/utils/grid/cellRenderers' -import { BCColumnSetFilter } from '@/components/BCDataGrid/components' +import { BCSelectFloatingFilter } from '@/components/BCDataGrid/components' import { useOrganizationStatuses } from '@/hooks/useOrganizations' import { usersColumnDefs } from '@/views/Admin/AdminMenu/components/_schema' import { t } from 'i18next' @@ -48,13 +48,11 @@ export const organizationsColDefs = (t) => [ valueGetter: (params) => params.data.orgStatus.status, cellRenderer: OrgStatusRenderer, cellClass: 'vertical-middle', - floatingFilterComponent: BCColumnSetFilter, + floatingFilterComponent: BCSelectFloatingFilter, floatingFilterComponentParams: { - apiOptionField: 'status', - apiQuery: useOrganizationStatuses, - key: 'org-status', - disableCloseOnSelect: false, - multiple: false + valueKey: 'status', + labelKey: 'status', + optionsQuery: useOrganizationStatuses }, suppressHeaderMenuButton: true } diff --git a/frontend/src/views/Transactions/_schema.js b/frontend/src/views/Transactions/_schema.js index 46bbbc82c..0785181ba 100644 --- a/frontend/src/views/Transactions/_schema.js +++ b/frontend/src/views/Transactions/_schema.js @@ -5,7 +5,7 @@ import { spacesFormatter } from '@/utils/formatters' import { TransactionStatusRenderer } from '@/utils/grid/cellRenderers' -import { BCColumnSetFilter } from '@/components/BCDataGrid/components' +import { BCSelectFloatingFilter } from '@/components/BCDataGrid/components' import { useTransactionStatuses } from '@/hooks/useTransactions' const prefixMap = { @@ -102,12 +102,11 @@ export const transactionsColDefs = (t) => [ headerName: t('txn:txnColLabels.status'), cellRenderer: TransactionStatusRenderer, cellClass: 'vertical-middle', - floatingFilterComponent: BCColumnSetFilter, + floatingFilterComponent: BCSelectFloatingFilter, floatingFilterComponentParams: { - apiOptionField: 'status', - apiQuery: useTransactionStatuses, - disableCloseOnSelect: false, - multiple: false + valueKey: 'status', + labelKey: 'status', + optionsQuery: useTransactionStatuses, }, suppressHeaderMenuButton: true, minWidth: 180,