diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fe13794b..1ffce0eef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,39 @@ ## Not released -## 2.3 +## 2.4 + +### 2.4.2 (2024-03-22) + +- Category widget UI: increase keyboard accessibility [#856](https://github.com/CartoDB/carto-react/pull/856) +- Category widget UI: change focus order to increase keyboard accessibility [#857](https://github.com/CartoDB/carto-react/pull/857) + +### 2.4.1 (2024-03-13) + +- Fix geometry edit in Feature Selection [#854](https://github.com/CartoDB/carto-react/pull/854) + +### 2.4.0 (2024-03-08) + +- Check invalid geometries for spatial filter [#847](https://github.com/CartoDB/carto-react/pull/847) + +### 2.3.14 (2024-03-07) + +- Fix PieWidgetUI keep two decimals always in default percent formatter [#850](https://github.com/CartoDB/carto-react/pull/850) + +### 2.3.13 (2024-03-06) + +- Add percentFormatter prop in PieWidgetUI, allowing to format percent values [#844](https://github.com/CartoDB/carto-react/pull/844) +- Fix Snowflake spatial index detection [#845](https://github.com/CartoDB/carto-react/pull/845) +- Fix TimeSeriesWidgetUI check undefined echart.getInstance().getDom() [#842](https://github.com/CartoDB/carto-react/pull/842) +- Update MultipleSelectField placeholder text color [#846](https://github.com/CartoDB/carto-react/pull/846) + +### 2.3.12 (2024-02-19) + +- Fix TimeSeriesContext exports [#840](https://github.com/CartoDB/carto-react/pull/840) + +### 2.3.11 (2024-02-13) + +- Keyboard accessibility improvements in Widgets and DS components [#835](https://github.com/CartoDB/carto-react/pull/835) ### 2.3.10 (2024-01-31) diff --git a/lerna.json b/lerna.json index d37a3a367..40b957495 100644 --- a/lerna.json +++ b/lerna.json @@ -4,5 +4,5 @@ ], "npmClient": "yarn", "useWorkspaces": true, - "version": "2.3.10" + "version": "2.4.2" } diff --git a/package.json b/package.json index fcd4be78a..9f906ac73 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "@turf/intersect": "^6.3.0", "@turf/invariant": "^6.3.0", "@turf/union": "^6.3.0", + "@turf/kinks": "^6.3.0", "dequal": "^2.0.2", "echarts": "^5.4.2", "echarts-for-react": "^3.0.2", diff --git a/packages/react-api/package.json b/packages/react-api/package.json index b88fdd266..0bc769247 100644 --- a/packages/react-api/package.json +++ b/packages/react-api/package.json @@ -1,6 +1,6 @@ { "name": "@carto/react-api", - "version": "2.3.10", + "version": "2.4.2", "description": "CARTO for React - Api", "author": "CARTO Dev Team", "keywords": [ @@ -68,9 +68,9 @@ "@babel/runtime": "^7.13.9" }, "peerDependencies": { - "@carto/react-core": "^2.3.10", - "@carto/react-redux": "^2.3.10", - "@carto/react-workers": "^2.3.10", + "@carto/react-core": "^2.4.2", + "@carto/react-redux": "^2.4.2", + "@carto/react-workers": "^2.4.2", "@deck.gl/carto": "^8.9.18", "@deck.gl/core": "^8.9.18", "@deck.gl/extensions": "^8.9.18", diff --git a/packages/react-api/src/hooks/useCartoLayerProps.js b/packages/react-api/src/hooks/useCartoLayerProps.js index 06dce1016..ab9c40515 100644 --- a/packages/react-api/src/hooks/useCartoLayerProps.js +++ b/packages/react-api/src/hooks/useCartoLayerProps.js @@ -1,6 +1,6 @@ import { useCallback } from 'react'; import { useSelector } from 'react-redux'; -import { selectSpatialFilter, selectViewport } from '@carto/react-redux'; +import { selectValidSpatialFilter, selectViewport } from '@carto/react-redux'; import useGeojsonFeatures from './useGeojsonFeatures'; import useTileFeatures from './useTileFeatures'; import { getDataFilterExtensionProps } from './dataFilterExtensionUtil'; @@ -18,7 +18,9 @@ export default function useCartoLayerProps({ viewporFeaturesDebounceTimeout = 250 }) { const viewport = useSelector(selectViewport); - const spatialFilter = useSelector((state) => selectSpatialFilter(state, source?.id)); + const spatialFilter = useSelector((state) => + selectValidSpatialFilter(state, source?.id) + ); const [onDataLoadForGeojson] = useGeojsonFeatures({ source, diff --git a/packages/react-auth/package.json b/packages/react-auth/package.json index da0296d67..ef1b4b4e4 100644 --- a/packages/react-auth/package.json +++ b/packages/react-auth/package.json @@ -1,6 +1,6 @@ { "name": "@carto/react-auth", - "version": "2.3.10", + "version": "2.4.2", "description": "CARTO for React - Auth", "author": "CARTO Dev Team", "keywords": [ @@ -68,7 +68,7 @@ "@babel/runtime": "^7.13.9" }, "peerDependencies": { - "@carto/react-core": "^2.3.10", + "@carto/react-core": "^2.4.2", "react": "17.x || 18.x", "react-dom": "17.x || 18.x" } diff --git a/packages/react-basemaps/package.json b/packages/react-basemaps/package.json index e32080d9e..abbb098e5 100644 --- a/packages/react-basemaps/package.json +++ b/packages/react-basemaps/package.json @@ -1,6 +1,6 @@ { "name": "@carto/react-basemaps", - "version": "2.3.10", + "version": "2.4.2", "description": "CARTO for React - Basemaps", "keywords": [ "carto", @@ -68,7 +68,7 @@ "@babel/runtime": "^7.13.9" }, "peerDependencies": { - "@carto/react-core": "^2.3.10", + "@carto/react-core": "^2.4.2", "@deck.gl/google-maps": "^8.9.18", "react": "17.x || 18.x", "react-dom": "17.x || 18.x" diff --git a/packages/react-core/__tests__/utils/columns.test.js b/packages/react-core/__tests__/utils/columns.test.js index 4534eb0c5..71e83f634 100644 --- a/packages/react-core/__tests__/utils/columns.test.js +++ b/packages/react-core/__tests__/utils/columns.test.js @@ -21,14 +21,20 @@ describe('getColumnNameFromGeoColumn', () => { describe('getSpatialIndexFromGeoColumn', () => { it('detect simple spatial index', () => { expect(getSpatialIndexFromGeoColumn('quadbin')).toStrictEqual('quadbin'); + expect(getSpatialIndexFromGeoColumn('QUADBIN')).toStrictEqual('quadbin'); expect(getSpatialIndexFromGeoColumn('h3')).toStrictEqual('h3'); + expect(getSpatialIndexFromGeoColumn('H3')).toStrictEqual('h3'); expect(getSpatialIndexFromGeoColumn('s2')).toStrictEqual('s2'); + expect(getSpatialIndexFromGeoColumn('S2')).toStrictEqual('s2'); }); it('detect prefix spatial index', () => { expect(getSpatialIndexFromGeoColumn('quadbin:abc')).toStrictEqual('quadbin'); + expect(getSpatialIndexFromGeoColumn('QUADBIN:ABC')).toStrictEqual('quadbin'); expect(getSpatialIndexFromGeoColumn('h3:abc')).toStrictEqual('h3'); + expect(getSpatialIndexFromGeoColumn('H3:abc')).toStrictEqual('h3'); expect(getSpatialIndexFromGeoColumn('s2:abc')).toStrictEqual('s2'); + expect(getSpatialIndexFromGeoColumn('S2:abc')).toStrictEqual('s2'); }); it('handle unsupported spatial index', () => { diff --git a/packages/react-core/package.json b/packages/react-core/package.json index 201b64731..f5d702d3d 100644 --- a/packages/react-core/package.json +++ b/packages/react-core/package.json @@ -1,6 +1,6 @@ { "name": "@carto/react-core", - "version": "2.3.10", + "version": "2.4.2", "description": "CARTO for React - Core", "author": "CARTO Dev Team", "keywords": [ diff --git a/packages/react-core/src/utils/columns.js b/packages/react-core/src/utils/columns.js index 57bd989e3..114029c2c 100644 --- a/packages/react-core/src/utils/columns.js +++ b/packages/react-core/src/utils/columns.js @@ -8,7 +8,7 @@ export function getColumnNameFromGeoColumn(geoColumn) { export function getSpatialIndexFromGeoColumn(geoColumn) { const parts = geoColumn.split(':'); return (parts.length === 1 || parts.length === 2) && - Object.values(SpatialIndex).includes(parts[0]) - ? parts[0] + Object.values(SpatialIndex).includes(parts[0].toLowerCase()) + ? parts[0].toLowerCase() : null; } diff --git a/packages/react-redux/package.json b/packages/react-redux/package.json index 0dc31637b..da011e425 100644 --- a/packages/react-redux/package.json +++ b/packages/react-redux/package.json @@ -1,6 +1,6 @@ { "name": "@carto/react-redux", - "version": "2.3.10", + "version": "2.4.2", "description": "CARTO for React - Redux", "author": "CARTO Dev Team", "keywords": [ @@ -67,8 +67,8 @@ "@babel/runtime": "^7.13.9" }, "peerDependencies": { - "@carto/react-core": "^2.3.10", - "@carto/react-workers": "^2.3.10", + "@carto/react-core": "^2.4.2", + "@carto/react-workers": "^2.4.2", "@deck.gl/carto": "^8.9.18", "@deck.gl/core": "^8.9.18", "@reduxjs/toolkit": "^1.5.0" diff --git a/packages/react-redux/src/index.d.ts b/packages/react-redux/src/index.d.ts index 5612e6a3d..6b190b665 100644 --- a/packages/react-redux/src/index.d.ts +++ b/packages/react-redux/src/index.d.ts @@ -1,13 +1,3 @@ export * from './slices/cartoSlice'; export * from './slices/oauthSlice'; - -export { - InitialCartoState, - CartoState, - InitialOauthState, - OauthState, - ViewState, - InitialCarto3State, - Source, - SourceFilters -} from './types'; +export * from './types'; diff --git a/packages/react-redux/src/slices/cartoSlice.d.ts b/packages/react-redux/src/slices/cartoSlice.d.ts index e4ada380e..6948f6374 100644 --- a/packages/react-redux/src/slices/cartoSlice.d.ts +++ b/packages/react-redux/src/slices/cartoSlice.d.ts @@ -169,4 +169,9 @@ export function selectSpatialFilter( sourceId?: string ): Feature | null; +export function selectValidSpatialFilter( + state: any, + sourceId?: string +): Feature | null; + export function selectFeatureSelectionMode(state: any): string | null; diff --git a/packages/react-redux/src/slices/cartoSlice.js b/packages/react-redux/src/slices/cartoSlice.js index babcf3202..a251a0f32 100644 --- a/packages/react-redux/src/slices/cartoSlice.js +++ b/packages/react-redux/src/slices/cartoSlice.js @@ -381,6 +381,15 @@ export const selectSpatialFilter = (state, sourceId) => { : spatialFilterGeometry; }; +/** + * Redux selector to select the spatial filter of a given sourceId or the root one. + * This selector returns null if the spatial filter is invalid (if it intersetcs itself) + */ +export const selectValidSpatialFilter = (state, sourceId) => { + const spatialFilter = selectSpatialFilter(state, sourceId); + return spatialFilter?.properties?.invalid ? null : spatialFilter; +}; + /** * Redux selector to select the feature selection mode based on if it's enabled */ diff --git a/packages/react-ui/__tests__/widgets/CategoryWidgetUI.test.js b/packages/react-ui/__tests__/widgets/CategoryWidgetUI.test.js index 5e2382eff..3fcb28836 100644 --- a/packages/react-ui/__tests__/widgets/CategoryWidgetUI.test.js +++ b/packages/react-ui/__tests__/widgets/CategoryWidgetUI.test.js @@ -62,6 +62,10 @@ describe('CategoryWidgetUI', () => { }); describe('events', () => { + beforeEach(() => { + HTMLElement.prototype.scrollIntoView = jest.fn(); + }); + test('category change', () => { const mockOnSelectedCategoriesChange = jest.fn(); render( @@ -132,13 +136,12 @@ describe('CategoryWidgetUI', () => { fireEvent.click(screen.getByText(/Search in 4 elements/)); fireEvent.click(screen.getByText(/Category 1/)); - fireEvent.click(screen.getByText(/Apply/)); + fireEvent.click(screen.getByTestId('primaryApplyButton')); fireEvent.click(screen.getByText(/Unlock/)); expect(mockOnSelectedCategoriesChange).toHaveBeenCalledTimes(2); }); test('search category', () => { - HTMLElement.prototype.scrollIntoView = jest.fn(); const mockOnSelectedCategoriesChange = jest.fn(); render( { fireEvent.click(screen.getByText(/Search in 4 elements/)); userEvent.type(screen.getByRole('textbox'), 'Category 1'); fireEvent.click(screen.getByText(/Category 1/)); - fireEvent.click(screen.getByText(/Apply/)); + fireEvent.click(screen.getByTestId('primaryApplyButton')); }); test('cancel search', () => { @@ -159,7 +162,7 @@ describe('CategoryWidgetUI', () => { expect(screen.getByText(/Search in 4 elements/)).toBeInTheDocument(); fireEvent.click(screen.getByText(/Search in 4 elements/)); - fireEvent.click(screen.getByText(/Cancel/)); + fireEvent.click(screen.getByTestId('primaryCancelButton')); }); test('searchable prop', () => { diff --git a/packages/react-ui/__tests__/widgets/FeatureSelectionWidgetUI.test.js b/packages/react-ui/__tests__/widgets/FeatureSelectionWidgetUI.test.js index 14d5665f5..ebfc0c4ed 100644 --- a/packages/react-ui/__tests__/widgets/FeatureSelectionWidgetUI.test.js +++ b/packages/react-ui/__tests__/widgets/FeatureSelectionWidgetUI.test.js @@ -123,11 +123,28 @@ describe('FeatureSelectionWidgetUI', () => { name: 'Mask' } }; + const INVALID_GEOM = { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [125.6, 10.1] + }, + properties: { + name: 'Mask', + invalid: true + } + }; + test('geometry is rendered correctly', () => { const rendered = render(); expect(rendered.getByText('Mask')).toBeDefined(); }); + test('invalid geometry is rendered correctly', () => { + const rendered = render(); + expect(rendered.getByRole('button', { name: 'Invalid geometry' })).toBeDefined(); + }); + test('geometry select event is raised correctly', () => { const onSelectGeometry = jest.fn(); diff --git a/packages/react-ui/package.json b/packages/react-ui/package.json index ef3ff6d86..588f9a01a 100644 --- a/packages/react-ui/package.json +++ b/packages/react-ui/package.json @@ -1,6 +1,6 @@ { "name": "@carto/react-ui", - "version": "2.3.10", + "version": "2.4.2", "description": "CARTO for React - UI", "author": "CARTO Dev Team", "keywords": [ @@ -82,7 +82,7 @@ "@babel/runtime": "^7.13.9" }, "peerDependencies": { - "@carto/react-core": "^2.3.10", + "@carto/react-core": "^2.4.2", "@emotion/react": "^11.10.6", "@emotion/styled": "^11.10.6", "@formatjs/intl-localematcher": "^0.4.0", diff --git a/packages/react-ui/src/components/molecules/MultipleSelectField/MultipleSelectField.js b/packages/react-ui/src/components/molecules/MultipleSelectField/MultipleSelectField.js index 5e5a0b1ac..2a64a20a4 100644 --- a/packages/react-ui/src/components/molecules/MultipleSelectField/MultipleSelectField.js +++ b/packages/react-ui/src/components/molecules/MultipleSelectField/MultipleSelectField.js @@ -92,7 +92,7 @@ const MultipleSelectField = forwardRef( {placeholder || intlConfig.formatMessage({ id: 'c4r.form.noneSelected' })} diff --git a/packages/react-ui/src/index.js b/packages/react-ui/src/index.js index 06f591a7a..aaf65a637 100644 --- a/packages/react-ui/src/index.js +++ b/packages/react-ui/src/index.js @@ -13,11 +13,16 @@ import LegendProportion from './widgets/legend/legend-types/LegendProportion'; import LegendRamp from './widgets/legend/legend-types/LegendRamp'; import ScatterPlotWidgetUI from './widgets/ScatterPlotWidgetUI/ScatterPlotWidgetUI'; import TimeSeriesWidgetUI from './widgets/TimeSeriesWidgetUI/TimeSeriesWidgetUI'; +import { + useTimeSeriesContext, + TimeSeriesProvider +} from './widgets/TimeSeriesWidgetUI/hooks/TimeSeriesContext'; import FeatureSelectionWidgetUI from './widgets/FeatureSelectionWidgetUI/FeatureSelectionWidgetUI'; import FeatureSelectionUIDropdown from './widgets/FeatureSelectionWidgetUI/FeatureSelectionUIDropdown'; import FeatureSelectionUIGeometryChips from './widgets/FeatureSelectionWidgetUI/FeatureSelectionUIGeometryChips'; import FeatureSelectionUIToggleButton from './widgets/FeatureSelectionWidgetUI/FeatureSelectionUIToggleButton'; import RangeWidgetUI from './widgets/RangeWidgetUI/RangeWidgetUI'; +import useTimeSeriesInteractivity from './widgets/TimeSeriesWidgetUI/hooks/useTimeSeriesInteractivity'; import ComparativeFormulaWidgetUI from './widgets/comparative/ComparativeFormulaWidgetUI/ComparativeFormulaWidgetUI'; import ComparativeCategoryWidgetUI from './widgets/comparative/ComparativeCategoryWidgetUI/ComparativeCategoryWidgetUI'; import { CHART_TYPES } from './widgets/TimeSeriesWidgetUI/utils/constants'; @@ -73,6 +78,9 @@ export { FeatureSelectionUIGeometryChips, FeatureSelectionUIToggleButton, TimeSeriesWidgetUI, + useTimeSeriesContext, + useTimeSeriesInteractivity, + TimeSeriesProvider, CHART_TYPES as TIME_SERIES_CHART_TYPES, TableWidgetUI, LegendWidgetUI, diff --git a/packages/react-ui/src/localization/en.js b/packages/react-ui/src/localization/en.js index ceea1b0d8..c9082407d 100644 --- a/packages/react-ui/src/localization/en.js +++ b/packages/react-ui/src/localization/en.js @@ -92,12 +92,17 @@ const locales = { remove: 'Remove', polygon: 'Polygon', point: 'Point', - lineString: 'Line' + lineString: 'Line', + invalid: 'Invalid geometry' }, pie: { clear: 'Clear', selectedItems: '{items, plural, one{# selected} other{# selected}}', allSelected: 'All' + }, + chartLegend: { + next: 'Next page', + prev: 'Previous page' } } } diff --git a/packages/react-ui/src/localization/es.js b/packages/react-ui/src/localization/es.js index e41421509..01396e5a7 100644 --- a/packages/react-ui/src/localization/es.js +++ b/packages/react-ui/src/localization/es.js @@ -97,6 +97,10 @@ const locales = { clear: 'Limpiar', selectedItems: '{items, plural, one {# seleccionado} other {# seleccionados}}', allSelected: 'Todos seleccionados' + }, + chartLegend: { + next: 'Página siguiente', + prev: 'Página anterior' } } } diff --git a/packages/react-ui/src/localization/id.js b/packages/react-ui/src/localization/id.js index e432656a0..1ba1895e2 100644 --- a/packages/react-ui/src/localization/id.js +++ b/packages/react-ui/src/localization/id.js @@ -99,6 +99,10 @@ const locales = { clear: 'Bersihkan', selectedItems: '{items, plural, one{# terpilih} other{# terpilih}}', allSelected: 'Semua terpilih' + }, + chartLegend: { + next: 'Next page', + prev: 'Previous page' } } } diff --git a/packages/react-ui/src/theme/sections/components/dataDisplay.js b/packages/react-ui/src/theme/sections/components/dataDisplay.js index fcd242e4d..66b9595e0 100644 --- a/packages/react-ui/src/theme/sections/components/dataDisplay.js +++ b/packages/react-ui/src/theme/sections/components/dataDisplay.js @@ -59,6 +59,12 @@ export const dataDisplayOverrides = { } } } + }, + + '& .MuiListItemButton-root': { + '&:focus-visible': { + backgroundColor: 'transparent' + } } }) } diff --git a/packages/react-ui/src/theme/sections/components/forms.js b/packages/react-ui/src/theme/sections/components/forms.js index e021e262b..26e909c48 100644 --- a/packages/react-ui/src/theme/sections/components/forms.js +++ b/packages/react-ui/src/theme/sections/components/forms.js @@ -194,6 +194,19 @@ export const formsOverrides = { }, '&.Mui-disabled::placeholder': { color: theme.palette.text.disabled + }, + // Remove focus on keyboard navigation as the parent element has focus + '&:focus-visible': { + outline: 'none !important', + boxShadow: 'none !important' + } + }, + + '&.Mui-disabled': { + cursor: 'not-allowed', + + '& .Mui-disabled': { + cursor: 'not-allowed' } }, @@ -332,6 +345,11 @@ export const formsOverrides = { }, '&.Mui-disabled::placeholder': { color: theme.palette.text.disabled + }, + // Remove focus on keyboard navigation as the parent element has focus + '&:focus-visible': { + outline: 'none !important', + boxShadow: 'none !important' } }, @@ -513,6 +531,12 @@ export const formsOverrides = { '&:focus': { background: 'transparent' }, + // Remove focus on keyboard navigation as the parent element has focus + '&:focus-visible': { + outline: 'none !important', + boxShadow: 'none !important' + }, + '& .MuiTypography-root': { whiteSpace: 'nowrap', overflow: 'hidden', @@ -532,13 +556,10 @@ export const formsOverrides = { size: 'small', fullWidth: true, popupIcon: , - clearIcon: , + clearIcon: , ChipProps: { color: 'default' }, limitTags: 1, componentsProps: { - popupIndicator: { - disabled: true - }, paper: { elevation: 8 } @@ -631,7 +652,11 @@ export const formsOverrides = { popupIndicator: ({ theme }) => ({ width: ICON_SIZE_MEDIUM, height: ICON_SIZE_MEDIUM, + cursor: 'text', + '&:hover': { + backgroundColor: 'transparent' + }, '&.MuiButtonBase-root': { color: theme.palette.text.secondary }, diff --git a/packages/react-ui/src/theme/sections/components/navigation.js b/packages/react-ui/src/theme/sections/components/navigation.js index 0365dddcb..09b3a6a63 100644 --- a/packages/react-ui/src/theme/sections/components/navigation.js +++ b/packages/react-ui/src/theme/sections/components/navigation.js @@ -1,7 +1,7 @@ import { ICON_SIZE_MEDIUM } from '../../themeConstants'; export const navigationOverrides = { - // Menu + // Menu Item MuiMenuItem: { styleOverrides: { root: ({ theme }) => ({ @@ -19,6 +19,9 @@ export const navigationOverrides = { '&:focus-visible': { // Solves a known Mui issue: https://github.com/mui/material-ui/issues/23747 backgroundColor: 'transparent', + outline: `none !important`, + boxShadow: `inset 0 0 0 2px ${theme.palette.primary.main} !important`, + borderRadius: theme.shape.borderRadius, '&:hover': { backgroundColor: theme.palette.action.hover @@ -107,6 +110,11 @@ export const navigationOverrides = { '&:hover': { borderBottomColor: theme.palette.text.primary }, + '&:focus-visible': { + outline: `none !important`, + boxShadow: `inset 0 0 0 2px ${theme.palette.primary.main} !important`, + borderRadius: `${theme.shape.borderRadius}px ${theme.shape.borderRadius}px 0 0` + }, '&.Mui-selected': { pointerEvents: 'none', diff --git a/packages/react-ui/src/theme/sections/components/surfaces.js b/packages/react-ui/src/theme/sections/components/surfaces.js index 1d94adb19..dc5d0a9ba 100644 --- a/packages/react-ui/src/theme/sections/components/surfaces.js +++ b/packages/react-ui/src/theme/sections/components/surfaces.js @@ -65,6 +65,10 @@ export const surfacesOverrides = { '&.Mui-disabled': { opacity: 1, color: theme.palette.text.disabled + }, + '&.Mui-focusVisible': { + backgroundColor: 'transparent', + borderRadius: theme.shape.borderRadius } }), expandIconWrapper: ({ theme }) => ({ diff --git a/packages/react-ui/src/theme/sections/cssBaseline.js b/packages/react-ui/src/theme/sections/cssBaseline.js index 1f90ce66b..81156e10e 100644 --- a/packages/react-ui/src/theme/sections/cssBaseline.js +++ b/packages/react-ui/src/theme/sections/cssBaseline.js @@ -3,6 +3,13 @@ import { commonPalette } from './palette'; import { themeTypography } from './typography'; export const CssBaseline = { + // Accessibility focus (keyboard only) + '*:focus-visible': { + outline: `2px solid ${commonPalette.primary.main} !important`, + boxShadow: `0 0 0 4px ${commonPalette.primary.background} !important`, + borderRadius: '4px' + }, + // Custom scrollbars '*::-webkit-scrollbar': { position: 'fixed' diff --git a/packages/react-ui/src/types.d.ts b/packages/react-ui/src/types.d.ts index 084aeca74..f21729de7 100644 --- a/packages/react-ui/src/types.d.ts +++ b/packages/react-ui/src/types.d.ts @@ -95,6 +95,7 @@ export type PieWidgetUI = { data: PieWidgetUIData; formatter?: Function; tooltipFormatter?: Function; + percentFormatter?: Function; height?: string; colors?: string[]; selectedCategories?: string[]; diff --git a/packages/react-ui/src/widgets/BarWidgetUI/BarWidgetUI.js b/packages/react-ui/src/widgets/BarWidgetUI/BarWidgetUI.js index 0c9e08927..b5523550c 100644 --- a/packages/react-ui/src/widgets/BarWidgetUI/BarWidgetUI.js +++ b/packages/react-ui/src/widgets/BarWidgetUI/BarWidgetUI.js @@ -248,6 +248,11 @@ function BarWidgetUI(props) { onSelectedBarsChange([], []); } }; + const handleClearPress = (e) => { + if (e.key === 'Enter') { + clearBars(); + } + }; const clickEvent = useCallback( (params) => { @@ -311,7 +316,12 @@ function BarWidgetUI(props) { : intlConfig.formatMessage({ id: 'c4r.widgets.bar.all' })} {selectedBars && selectedBars.length > 0 && ( - clearBars()} underline='hover'> + clearBars()} + onKeyDown={handleClearPress} + underline='hover' + tabIndex={0} + > {intlConfig.formatMessage({ id: 'c4r.widgets.bar.clear' })} )} diff --git a/packages/react-ui/src/widgets/CategoryWidgetUI/CategoryWidgetUI.js b/packages/react-ui/src/widgets/CategoryWidgetUI/CategoryWidgetUI.js index 3e74f4bdf..26fb79f57 100644 --- a/packages/react-ui/src/widgets/CategoryWidgetUI/CategoryWidgetUI.js +++ b/packages/react-ui/src/widgets/CategoryWidgetUI/CategoryWidgetUI.js @@ -7,7 +7,8 @@ import { InputAdornment, Divider, TextField, - Tooltip + Tooltip, + Box } from '@mui/material'; import { useIntl } from 'react-intl'; @@ -22,7 +23,9 @@ import { LinkAsButton, OptionsSelectedBar, ProgressBar, - CategoriesRoot + CategoriesRoot, + CategoryLabelWrapper, + HiddenButton } from './CategoryWidgetUI.styled'; import SearchIcon from '../../assets/icons/SearchIcon'; import useImperativeIntl from '../../hooks/useImperativeIntl'; @@ -59,6 +62,7 @@ function CategoryWidgetUI(props) { const [tempBlockedCategories, setTempBlockedCategories] = useState(false); const [animValues, setAnimValues] = useState([]); const requestRef = useRef(); + const searchRef = useRef(); const prevAnimValues = usePrevious(animValues); const referencedPrevAnimValues = useRef(); const { showSkeleton } = useSkeleton(isLoading); @@ -92,15 +96,30 @@ function CategoryWidgetUI(props) { const handleClearClicked = () => { props.onSelectedCategoriesChange([]); }; + const handleClearPress = (e) => { + if (e.key === 'Enter') { + handleClearClicked(); + } + }; const handleUnblockClicked = () => { props.onSelectedCategoriesChange([]); setBlockedCategories([]); }; + const handleUnblockPress = (e) => { + if (e.key === 'Enter') { + handleUnblockClicked(); + } + }; const handleBlockClicked = () => { setBlockedCategories(sortBlockedSameAsData(selectedCategories)); }; + const handleBlockPress = (e) => { + if (e.key === 'Enter') { + handleBlockClicked(); + } + }; const handleApplyClicked = () => { const blockedCategoriesOrdered = sortBlockedSameAsData(tempBlockedCategories); @@ -111,6 +130,11 @@ function CategoryWidgetUI(props) { setShowAll(false); setSearchValue(''); }; + const handleApplyPress = (e) => { + if (e.key === 'Enter') { + handleApplyClicked(); + } + }; const handleCancelClicked = () => { setSearchValue(''); @@ -282,6 +306,12 @@ function CategoryWidgetUI(props) { } }, [animation, sortedData]); + useEffect(() => { + if (showAll && searchRef.current) { + searchRef.current.focus(); + } + }, [showAll, searchRef]); + // Separated to simplify the widget layout but inside the main component to avoid passing all dependencies const CategoryItem = (props) => { const { data, onCategoryClick } = props; @@ -303,26 +333,38 @@ function CategoryWidgetUI(props) { }; }, []); + const onCategoryPress = (e) => { + if (e.key === 'Enter') { + onCategoryClick(); + } + }; + const unselected = !showAll && selectedCategories.length > 0 && selectedCategories.indexOf(data.name) === -1; + return ( {}} + onKeyDown={filterable ? onCategoryPress : () => {}} selectable={filterable} unselected={unselected} name={data.name === REST_CATEGORY ? REST_CATEGORY : ''} + tabIndex={filterable ? 0 : -1} > {filterable && showAll && ( - - + + )} - +
-
+
); }; @@ -371,21 +413,42 @@ function CategoryWidgetUI(props) { : intlConfig.formatMessage({ id: 'c4r.widgets.category.all' })} {showAll ? ( - + {intlConfig.formatMessage({ id: 'c4r.widgets.category.apply' })} ) : blockedCategories.length > 0 ? ( - + {intlConfig.formatMessage({ id: 'c4r.widgets.category.unlock' })} ) : ( selectedCategories.length > 0 && ( - + {intlConfig.formatMessage({ id: 'c4r.widgets.category.lock' })} - + {intlConfig.formatMessage({ id: 'c4r.widgets.category.clear' })} @@ -398,7 +461,9 @@ function CategoryWidgetUI(props) { ) }} + inputProps={{ + tabIndex: 0, + ref: searchRef + }} /> + + {intlConfig.formatMessage({ id: 'c4r.widgets.category.cancel' })} + )} @@ -423,36 +495,50 @@ function CategoryWidgetUI(props) { /> )) ) : ( - <> + {intlConfig.formatMessage({ id: 'c4r.widgets.category.noResults' })} - + {intlConfig.formatMessage( { id: 'c4r.widgets.category.noResultsMessage' }, { searchValue } )} - + )} + {showAll && ( + + {intlConfig.formatMessage({ id: 'c4r.widgets.category.apply' })} + + )} {data.length > maxItems && searchable ? ( showAll ? ( - + + + ) : ( - + + + ) ) : null} diff --git a/packages/react-ui/src/widgets/CategoryWidgetUI/CategoryWidgetUI.styled.js b/packages/react-ui/src/widgets/CategoryWidgetUI/CategoryWidgetUI.styled.js index 4a11a9908..31e04ac0c 100644 --- a/packages/react-ui/src/widgets/CategoryWidgetUI/CategoryWidgetUI.styled.js +++ b/packages/react-ui/src/widgets/CategoryWidgetUI/CategoryWidgetUI.styled.js @@ -1,12 +1,12 @@ -import { Box, Grid, Link, styled } from '@mui/material'; +import { Box, Button, Grid, Link, styled } from '@mui/material'; import Typography from '../../components/atoms/Typography'; const REST_CATEGORY = '__rest__'; -export const CategoriesWrapper = styled(Grid)(({ theme: { spacing } }) => ({ - maxHeight: spacing(40), +export const CategoriesWrapper = styled(Grid)(({ theme }) => ({ + maxHeight: theme.spacing(40), overflow: 'auto', - padding: spacing(0, 1, 1, 0) + padding: 0 })); export const CategoryItemGroup = styled(Grid, { @@ -14,6 +14,19 @@ export const CategoryItemGroup = styled(Grid, { })(({ theme, selectable, name, unselected }) => { return { flexDirection: 'row', + maxWidth: '100%', + padding: theme.spacing(0.5, 0.25), + margin: 0, + + '> .MuiGrid-item': { + paddingTop: 0, + paddingLeft: 0 + }, + '&:focus-visible': { + outline: `none !important`, + boxShadow: `inset 0 0 0 2px ${theme.palette.primary.main} !important` + }, + ...(unselected && { color: theme.palette.text.disabled, @@ -55,7 +68,7 @@ export const OptionsSelectedBar = styled(Grid)(({ theme: { spacing, palette } }) export const ProgressBar = styled(Grid)(({ theme }) => ({ height: theme.spacing(0.5), width: '100%', - margin: theme.spacing(0.5, 0, 1, 0), + margin: theme.spacing(0.5, 0, 0.25, 0), borderRadius: theme.spacing(0.5), backgroundColor: theme.palette.action.disabledBackground, @@ -69,6 +82,16 @@ export const ProgressBar = styled(Grid)(({ theme }) => ({ } })); +export const CategoryLabelWrapper = styled(Grid, { + shouldForwardProp: (prop) => prop !== 'isSelectable' +})(({ theme, isSelectable }) => { + return { + ...(isSelectable && { + width: `calc(100% - ${theme.spacing(4)})` + }) + }; +}); + export const CategoryLabel = styled(Typography)(({ theme }) => ({ fontWeight: theme.typography.fontWeightBold, marginRight: theme.spacing(2) @@ -86,3 +109,19 @@ export const LinkAsButton = styled(Link)(({ theme }) => ({ export const CategoriesRoot = styled(Box)(({ theme }) => ({ ...theme.typography.body2 })); + +export const HiddenButton = styled(Button)(({ theme }) => ({ + position: 'absolute', + left: '-999px', + top: '-1px', + width: '1px', + height: '1px', + display: 'inline-flex', + + '&:focus-visible': { + position: 'static', + width: 'auto', + height: 'auto', + marginTop: theme.spacing(2) + } +})); diff --git a/packages/react-ui/src/widgets/ChartLegend.js b/packages/react-ui/src/widgets/ChartLegend.js index 129dfa249..a0598c7a4 100644 --- a/packages/react-ui/src/widgets/ChartLegend.js +++ b/packages/react-ui/src/widgets/ChartLegend.js @@ -1,8 +1,11 @@ import React, { useState, useRef, useEffect, useCallback } from 'react'; -import { Box, styled, IconButton, useTheme } from '@mui/material'; +import { useIntl } from 'react-intl'; + +import { Box, styled, IconButton, useTheme, Tooltip } from '@mui/material'; import { ChevronLeft, ChevronRight } from '@mui/icons-material'; import Typography from '../components/atoms/Typography'; +import useImperativeIntl from '../hooks/useImperativeIntl'; const Legend = styled(Box)(({ theme }) => ({ position: 'relative', @@ -15,7 +18,10 @@ const ItemsContainer = styled(Box)(({ theme }) => ({ display: 'flex', flexDirection: 'row', overflowX: 'hidden', - gap: theme.spacing(2) + gap: theme.spacing(2), + // Accessibility tweak to display focus indicator properly + padding: theme.spacing(0.5), + margin: theme.spacing(-0.5) })); const Item = styled(Box)(({ theme }) => ({ @@ -68,6 +74,9 @@ export default function ChartLegend({ series, selectedCategories, onCategoryClic const containerRef = useRef(null); const showMoreButtonsRef = useRef(null); + const intl = useIntl(); + const intlConfig = useImperativeIntl(intl); + const handleClickRight = () => { setOffset(offset + 1); }; @@ -76,6 +85,12 @@ export default function ChartLegend({ series, selectedCategories, onCategoryClic setOffset(Math.max(offset - 1)); }; + const onCategoryPress = (e, categoryName) => { + if (e.key === 'Enter') { + onCategoryClick(categoryName); + } + }; + const updateMaxWidth = useCallback(() => { const overflowing = containerRef.current && @@ -133,7 +148,11 @@ export default function ChartLegend({ series, selectedCategories, onCategoryClic return ( onCategoryClick(category.name) : undefined} + onKeyDown={ + onCategoryClick ? (e) => onCategoryPress(e, category.name) : undefined + } style={{ pointerEvents: !onCategoryClick || category.name === 'Others' ? 'none' : undefined @@ -153,12 +172,20 @@ export default function ChartLegend({ series, selectedCategories, onCategoryClic {(overflowing || offset > 0) && ( - - - - - - + + + + + + + + + + )} diff --git a/packages/react-ui/src/widgets/FeatureSelectionWidgetUI/FeatureSelectionUIGeometryChips.js b/packages/react-ui/src/widgets/FeatureSelectionWidgetUI/FeatureSelectionUIGeometryChips.js index 23ded43c0..33b402ffe 100644 --- a/packages/react-ui/src/widgets/FeatureSelectionWidgetUI/FeatureSelectionUIGeometryChips.js +++ b/packages/react-ui/src/widgets/FeatureSelectionWidgetUI/FeatureSelectionUIGeometryChips.js @@ -1,4 +1,4 @@ -import { Cancel } from '@mui/icons-material'; +import { Cancel, ErrorOutline } from '@mui/icons-material'; import { Box, Chip, List, ListItem, Tooltip, styled } from '@mui/material'; import PropTypes from 'prop-types'; import { useIntl } from 'react-intl'; @@ -9,7 +9,7 @@ const ChipList = styled(List)(({ theme }) => ({ display: 'flex', alignItems: 'center', marginLeft: theme.spacing(1.5), - padding: 0, + padding: theme.spacing(0.5), overflowX: 'auto', maxWidth: '100%', scrollbarWidth: 'none', @@ -80,13 +80,30 @@ function FeatureSelectionUIGeometryChips({ {features.map((geometry, index) => { const isDisabled = geometry.properties?.disabled; - const tooltipText = isDisabled - ? disabledChipTooltip || chipTooltip - : showDeleteTooltip - ? intlConfig.formatMessage({ - id: `c4r.widgets.featureSelection.remove` - }) - : chipTooltip; + const isInvalid = geometry.properties?.invalid; + + let tooltipText = chipTooltip; + if (isDisabled) { + tooltipText = disabledChipTooltip || chipTooltip; + } + if (isInvalid) { + tooltipText = intlConfig.formatMessage({ + id: `c4r.widgets.featureSelection.invalid` + }); + } + if (showDeleteTooltip) { + tooltipText = intlConfig.formatMessage({ + id: `c4r.widgets.featureSelection.remove` + }); + } + + let color = 'secondary'; + if (isDisabled) { + color = 'default'; + } + if (isInvalid) { + color = 'error'; + } return ( @@ -98,11 +115,12 @@ function FeatureSelectionUIGeometryChips({ onSelectGeometry(geometry)} onDelete={ onDeleteGeometry ? () => onDeleteGeometry(geometry) : undefined } + icon={isInvalid ? : undefined} deleteIcon={ setShowDeleteTooltip(true)} diff --git a/packages/react-ui/src/widgets/HistogramWidgetUI/HistogramWidgetUI.js b/packages/react-ui/src/widgets/HistogramWidgetUI/HistogramWidgetUI.js index 3d0bedcd5..114bda1ba 100644 --- a/packages/react-ui/src/widgets/HistogramWidgetUI/HistogramWidgetUI.js +++ b/packages/react-ui/src/widgets/HistogramWidgetUI/HistogramWidgetUI.js @@ -270,6 +270,12 @@ function HistogramWidgetUI({ 0 ); + const handleClearPress = (e) => { + if (e.key === 'Enter') { + onSelectedBarsChange([]); + } + }; + if (showSkeleton) return ; return ( @@ -285,7 +291,12 @@ function HistogramWidgetUI({ : intlConfig.formatMessage({ id: 'c4r.widgets.histogram.all' })} {selectedBars.length > 0 && ( - onSelectedBarsChange([])} underline='hover'> + onSelectedBarsChange([])} + onKeyDown={handleClearPress} + underline='hover' + tabIndex={0} + > {intlConfig.formatMessage({ id: 'c4r.widgets.histogram.clear' })} )} diff --git a/packages/react-ui/src/widgets/PieWidgetUI/PieWidgetUI.js b/packages/react-ui/src/widgets/PieWidgetUI/PieWidgetUI.js index db6bc97f8..570952af5 100644 --- a/packages/react-ui/src/widgets/PieWidgetUI/PieWidgetUI.js +++ b/packages/react-ui/src/widgets/PieWidgetUI/PieWidgetUI.js @@ -49,6 +49,7 @@ function PieWidgetUI({ data = [], formatter, tooltipFormatter, + percentFormatter, height, width, labels, @@ -67,7 +68,16 @@ function PieWidgetUI({ const { showSkeleton } = useSkeleton(isLoading); const intl = useIntl(); const intlConfig = useImperativeIntl(intl); - + const _percentFormatter = useMemo( + () => + percentFormatter || + ((v) => + `${intl.formatNumber(v, { + maximumFractionDigits: 2, + minimumFractionDigits: 2 + })}%`), + [intl, percentFormatter] + ); // Tooltip const tooltipOptions = useMemo( () => ({ @@ -77,14 +87,17 @@ function PieWidgetUI({ textStyle: { color: theme.palette.common.white }, confine: true, formatter: - !!tooltipFormatter && ((params) => tooltipFormatter({ ...params, formatter })) + !!tooltipFormatter && + ((params) => + tooltipFormatter({ ...params, formatter, percentFormatter: _percentFormatter })) }), [ theme.spacingValue, theme.palette.black, theme.palette.common.white, tooltipFormatter, - formatter + formatter, + _percentFormatter ] ); @@ -199,6 +212,11 @@ function PieWidgetUI({ const handleClearSelectedCategories = () => { onSelectedCategoriesChange([]); }; + const handleClearPress = (e) => { + if (e.key === 'Enter') { + handleClearSelectedCategories(); + } + }; if (showSkeleton) return ; @@ -215,7 +233,12 @@ function PieWidgetUI({ : intlConfig.formatMessage({ id: 'c4r.widgets.pie.allSelected' })} {selectedCategories.length > 0 && ( - + {intlConfig.formatMessage({ id: 'c4r.widgets.pie.clear' })} )} @@ -223,7 +246,11 @@ function PieWidgetUI({ )} - +

${params.name}

-

${value} (${params.percent}%)

+

${value} (${percentage})

`.trim(); } diff --git a/packages/react-ui/src/widgets/PieWidgetUI/components/PieCentralText.js b/packages/react-ui/src/widgets/PieWidgetUI/components/PieCentralText.js index a5e121335..dbb3f3569 100644 --- a/packages/react-ui/src/widgets/PieWidgetUI/components/PieCentralText.js +++ b/packages/react-ui/src/widgets/PieWidgetUI/components/PieCentralText.js @@ -29,7 +29,7 @@ const MarkerColor = styled(Box)(({ theme }) => ({ height: theme.spacing(1) })); -function PieCentralText({ data, selectedCategories }) { +function PieCentralText({ data, selectedCategories, formatter }) { const [selectedItem, setSelectedItem] = useState({}); // Select the largest category to display in CentralText and calculate its percentage from the total @@ -57,11 +57,11 @@ function PieCentralText({ data, selectedCategories }) { sumValue += category.value; } - const percentage = calculatePercentage(category.value, sumValue); + const percentage = calculatePercentage(category.value, sumValue, formatter); category.percentage = percentage; return category; - }, [data, selectedCategories]); + }, [data, selectedCategories, formatter]); useEffect(() => { if (topSelectedCategory) { @@ -96,7 +96,8 @@ PieCentralText.propTypes = { color: PropTypes.string }) ), - selectedCategories: PropTypes.array + selectedCategories: PropTypes.array, + formatter: PropTypes.func }; export default PieCentralText; diff --git a/packages/react-ui/src/widgets/RangeWidgetUI/RangeWidgetUI.js b/packages/react-ui/src/widgets/RangeWidgetUI/RangeWidgetUI.js index 971476dc6..e32aa08b1 100644 --- a/packages/react-ui/src/widgets/RangeWidgetUI/RangeWidgetUI.js +++ b/packages/react-ui/src/widgets/RangeWidgetUI/RangeWidgetUI.js @@ -164,6 +164,12 @@ function RangeWidgetUI({ data, min, max, limits, onSelectedRangeChange, isLoadin changeSliderValues([min, max]); }; + const handleClearPress = (e) => { + if (e.key === 'Enter') { + resetSlider(); + } + }; + if (showSkeleton) { return ; } @@ -173,7 +179,13 @@ function RangeWidgetUI({ data, min, max, limits, onSelectedRangeChange, isLoadin {hasBeenModified && ( - + {intlConfig.formatMessage({ id: 'c4r.widgets.range.clear' })} diff --git a/packages/react-ui/src/widgets/TimeSeriesWidgetUI/TimeSeriesWidgetUI.js b/packages/react-ui/src/widgets/TimeSeriesWidgetUI/TimeSeriesWidgetUI.js index ea3f9b0a9..b899f6bb8 100644 --- a/packages/react-ui/src/widgets/TimeSeriesWidgetUI/TimeSeriesWidgetUI.js +++ b/packages/react-ui/src/widgets/TimeSeriesWidgetUI/TimeSeriesWidgetUI.js @@ -304,6 +304,11 @@ function TimeSeriesWidgetUIContent({ setTimeWindow([]); onSelectedCategoriesChange?.([]); }; + const handleClearPress = (e) => { + if (e.key === 'Enter') { + handleClear(); + } + }; const handleCategoryClick = useCallback( (category) => { @@ -340,6 +345,8 @@ function TimeSeriesWidgetUIContent({ variant='caption' style={{ cursor: 'pointer' }} onClick={handleClear} + onKeyDown={handleClearPress} + tabIndex={0} underline='hover' > {intl.formatMessage({ id: 'c4r.widgets.timeSeries.clear' })} diff --git a/packages/react-ui/src/widgets/TimeSeriesWidgetUI/components/TimeSeriesChart.js b/packages/react-ui/src/widgets/TimeSeriesWidgetUI/components/TimeSeriesChart.js index cf4a73c43..765d67778 100644 --- a/packages/react-ui/src/widgets/TimeSeriesWidgetUI/components/TimeSeriesChart.js +++ b/packages/react-ui/src/widgets/TimeSeriesWidgetUI/components/TimeSeriesChart.js @@ -248,7 +248,7 @@ export default function TimeSeriesChart({ return; } - const element = echartsInstance.getDom().parentElement; + const element = echartsInstance?.getDom()?.parentElement; if (!element) return; diff --git a/packages/react-ui/src/widgets/WrapperWidgetUI.js b/packages/react-ui/src/widgets/WrapperWidgetUI.js index 2d2673901..06ef08bce 100644 --- a/packages/react-ui/src/widgets/WrapperWidgetUI.js +++ b/packages/react-ui/src/widgets/WrapperWidgetUI.js @@ -69,7 +69,8 @@ const HeaderButton = styled(Button, { alignItems: 'flex-start', justifyContent: 'flex-start', height: 'auto', - minHeight: theme.spacing(4), + minHeight: 'auto', + marginBottom: theme.spacing(1), cursor: expandable ? 'pointer' : 'default', '& .MuiButton-startIcon': { @@ -198,6 +199,7 @@ function WrapperWidgetUI(props) { props.expandable && {expanded ? : } } onClick={handleExpandClick} + tabIndex={props.expandable ? 0 : -1} > @@ -265,6 +267,7 @@ function WrapperWidgetUI(props) { )} + {/* TODO: check collapse error */} diff --git a/packages/react-ui/src/widgets/utils/chartUtils.js b/packages/react-ui/src/widgets/utils/chartUtils.js index 3815a941b..7915d0ab1 100644 --- a/packages/react-ui/src/widgets/utils/chartUtils.js +++ b/packages/react-ui/src/widgets/utils/chartUtils.js @@ -94,11 +94,12 @@ export function findLargestCategory(array) { } // Calculate the percentage of a value in relation to a total -export function calculatePercentage(value, total) { - if (total === 0) { - return '0.00%'; // Avoid division by zero +export function calculatePercentage(value, total, formatter) { + let percentage = 0; + + if (total !== 0) { + percentage = (value / total) * 100; } - const percentage = ((value / total) * 100).toFixed(2); // Limit to two decimals - return `${percentage}%`; + return formatter ? formatter(percentage) : `${percentage}%`; } diff --git a/packages/react-ui/storybook/stories/organisms/AppBar.stories.js b/packages/react-ui/storybook/stories/organisms/AppBar.stories.js index a679c32d1..de1caa950 100644 --- a/packages/react-ui/storybook/stories/organisms/AppBar.stories.js +++ b/packages/react-ui/storybook/stories/organisms/AppBar.stories.js @@ -157,7 +157,11 @@ const commonArgs = { brandText: 'CARTO', secondaryText: 'Some text', showBurgerMenu: true, - brandLogo: + brandLogo: ( + + + + ) }; export const Basic = Template.bind({}); Basic.args = { ...commonArgs }; diff --git a/packages/react-ui/storybook/stories/organisms/Menu.stories.js b/packages/react-ui/storybook/stories/organisms/Menu.stories.js index 53f82dda6..17917c03d 100644 --- a/packages/react-ui/storybook/stories/organisms/Menu.stories.js +++ b/packages/react-ui/storybook/stories/organisms/Menu.stories.js @@ -1,5 +1,13 @@ -import React from 'react'; -import { Divider, ListItemIcon, ListItemText, MenuItem, MenuList } from '@mui/material'; +import React, { useState } from 'react'; +import { + Divider, + ListItemIcon, + ListItemText, + MenuItem, + MenuList, + Menu, + Box +} from '@mui/material'; import { CloudOutlined, ContentCopyOutlined, @@ -38,7 +46,79 @@ const options = { }; export default options; -const Template = ({ label, ...args }) => { +const TemplateMenu = ({ label, ...args }) => { + const [anchorEl, setAnchorEl] = useState(null); + + const open = Boolean(anchorEl); + + const openDropdown = (event) => { + setAnchorEl(event.currentTarget); + }; + + const closeDropdown = () => { + setAnchorEl(null); + }; + + return ( + + + + + + + + {label} + + ⌘X + + + + + + + {label} + + ⌘C + + + + + + + {label} + + ⌘V + + + + + + + + {label} + + + + ); +}; +const TemplateMenuList = ({ label, ...args }) => { return ( @@ -105,7 +185,10 @@ const DocTemplate = () => { const commonArgs = { label: 'Label', dense: false }; -export const Playground = Template.bind({}); -Playground.args = { ...commonArgs }; - export const Guide = DocTemplate.bind({}); + +export const MenuWrapper = TemplateMenu.bind({}); +MenuWrapper.args = { ...commonArgs }; + +export const MenuListWrapper = TemplateMenuList.bind({}); +MenuListWrapper.args = { ...commonArgs }; diff --git a/packages/react-ui/storybook/stories/widgetsUI/CategoryWidgetUI.stories.js b/packages/react-ui/storybook/stories/widgetsUI/CategoryWidgetUI.stories.js index ff8503dc4..5df265218 100644 --- a/packages/react-ui/storybook/stories/widgetsUI/CategoryWidgetUI.stories.js +++ b/packages/react-ui/storybook/stories/widgetsUI/CategoryWidgetUI.stories.js @@ -60,7 +60,7 @@ const LoadingTemplate = (args) => { ); }; -const data = [...Array(7)].map((_, idx) => ({ +const data = [...Array(30)].map((_, idx) => ({ name: `Category ${idx + 1}`, value: idx * 100 })); diff --git a/packages/react-ui/storybook/stories/widgetsUI/PieWidgetUI.stories.js b/packages/react-ui/storybook/stories/widgetsUI/PieWidgetUI.stories.js index 09fed5e7c..47766bbd1 100644 --- a/packages/react-ui/storybook/stories/widgetsUI/PieWidgetUI.stories.js +++ b/packages/react-ui/storybook/stories/widgetsUI/PieWidgetUI.stories.js @@ -128,3 +128,15 @@ CollapseMoreThan12Categories.args = CollapseCategoriesProps; export const Loading = LoadingTemplate.bind({}); const LoadingProps = { data: dataDefault, isLoading: true }; Loading.args = LoadingProps; + +export const CustomPercentFormatter = Template.bind({}); +const CustomPercentFormatterProps = { + data: dataDefault, + formatter: (v) => `${v} units`, + percentFormatter: (v) => + `${Intl.NumberFormat('en', { + maximumFractionDigits: 3, + minimumFractionDigits: 3 + }).format(v)}%` +}; +CustomPercentFormatter.args = CustomPercentFormatterProps; diff --git a/packages/react-ui/storybook/stories/widgetsUI/WrapperWidgetUI.stories.js b/packages/react-ui/storybook/stories/widgetsUI/WrapperWidgetUI.stories.js index 3bcd918a3..c1e00b6cf 100644 --- a/packages/react-ui/storybook/stories/widgetsUI/WrapperWidgetUI.stories.js +++ b/packages/react-ui/storybook/stories/widgetsUI/WrapperWidgetUI.stories.js @@ -1,4 +1,5 @@ import React from 'react'; +import { IntlProvider } from 'react-intl'; import ColorizeIcon from '@mui/icons-material/Colorize'; import MenuIcon from '@mui/icons-material/Menu'; import AddLocationIcon from '@mui/icons-material/AddLocation'; @@ -31,14 +32,16 @@ const options = { export default options; const Template = (args) => ( - -
Your Content
-
+ + +
Your Content
+
+
); const ResponsiveTemplate = (args) => { return ( - <> + @@ -54,7 +57,7 @@ const ResponsiveTemplate = (args) => {
Your Content
- +
); }; @@ -192,27 +195,29 @@ WithActionsTooltip.args = { }; export const BigScrollableContent = (args) => ( - -
-

- Note: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam, mi nibh - fames rhoncus id ultricies. Faucibus enim commodo morbi amet sit eget. Ut - pellentesque tellus iaculis diam. Ornare convallis dictum purus quisque nisl. -

-

- Vivamus imperdiet, urna eu blandit lobortis, tortor risus sodales urna, sit amet - tempor eros elit faucibus nulla. Donec vel tellus nec nibh molestie hendrerit. - Donec nulla massa, interdum ut nisl non, sollicitudin condimentum leo. Integer - eget accumsan sem. Aliquam tincidunt turpis et leo ac. -

-

- Vivamus imperdiet, urna eu blandit lobortis, tortor risus sodales urna, sit amet - tempor eros elit faucibus nulla. Donec vel tellus nec nibh molestie hendrerit. - Donec nulla massa, interdum ut nisl non, sollicitudin condimentum leo. Integer - eget accumsan sem. Aliquam tincidunt turpis et leo ac. -

-
-
+ + +
+

+ Note: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam, mi nibh + fames rhoncus id ultricies. Faucibus enim commodo morbi amet sit eget. Ut + pellentesque tellus iaculis diam. Ornare convallis dictum purus quisque nisl. +

+

+ Vivamus imperdiet, urna eu blandit lobortis, tortor risus sodales urna, sit amet + tempor eros elit faucibus nulla. Donec vel tellus nec nibh molestie hendrerit. + Donec nulla massa, interdum ut nisl non, sollicitudin condimentum leo. Integer + eget accumsan sem. Aliquam tincidunt turpis et leo ac. +

+

+ Vivamus imperdiet, urna eu blandit lobortis, tortor risus sodales urna, sit amet + tempor eros elit faucibus nulla. Donec vel tellus nec nibh molestie hendrerit. + Donec nulla massa, interdum ut nisl non, sollicitudin condimentum leo. Integer + eget accumsan sem. Aliquam tincidunt turpis et leo ac. +

+
+
+
); BigScrollableContent.args = { title: 'Big scrollable content', diff --git a/packages/react-widgets/package.json b/packages/react-widgets/package.json index 171bcd9cd..aee4e81e5 100644 --- a/packages/react-widgets/package.json +++ b/packages/react-widgets/package.json @@ -1,6 +1,6 @@ { "name": "@carto/react-widgets", - "version": "2.3.10", + "version": "2.4.2", "description": "CARTO for React - Widgets", "author": "CARTO Dev Team", "keywords": [ @@ -69,11 +69,11 @@ "@babel/runtime": "^7.13.9" }, "peerDependencies": { - "@carto/react-api": "^2.3.10", - "@carto/react-core": "^2.3.10", - "@carto/react-redux": "^2.3.10", - "@carto/react-ui": "^2.3.10", - "@carto/react-workers": "^2.3.10", + "@carto/react-api": "^2.4.2", + "@carto/react-core": "^2.4.2", + "@carto/react-redux": "^2.4.2", + "@carto/react-ui": "^2.4.2", + "@carto/react-workers": "^2.4.2", "@deck.gl/core": "^8.9.18", "@deck.gl/layers": "^8.9.18", "@emotion/react": "^11.10.6", @@ -81,6 +81,7 @@ "@mui/material": "^5.11.16", "@nebula.gl/edit-modes": "^1.0.4", "@nebula.gl/layers": "^1.0.4", + "@turf/kinks": "^6.3.0", "dequal": "^2.0.2", "react": "17.x || 18.x", "react-dom": "17.x || 18.x", diff --git a/packages/react-widgets/src/hooks/useWidgetFetch.js b/packages/react-widgets/src/hooks/useWidgetFetch.js index df2e51fe3..92287814a 100644 --- a/packages/react-widgets/src/hooks/useWidgetFetch.js +++ b/packages/react-widgets/src/hooks/useWidgetFetch.js @@ -6,7 +6,7 @@ import { } from '@carto/react-core'; import { selectAreFeaturesReadyForSource, - selectSpatialFilter, + selectValidSpatialFilter, selectViewport } from '@carto/react-redux'; import { dequal } from 'dequal'; @@ -85,7 +85,9 @@ export default function useWidgetFetch( ); const viewport = useSelector(selectViewport); - const spatialFilter = useSelector((state) => selectSpatialFilter(state, dataSource)); + const spatialFilter = useSelector((state) => + selectValidSpatialFilter(state, dataSource) + ); const geometryToIntersect = useMemo( () => selectGeometryToIntersect(global, viewport, spatialFilter), [global, viewport, spatialFilter] diff --git a/packages/react-widgets/src/index.d.ts b/packages/react-widgets/src/index.d.ts index 739f25f82..7807de82d 100644 --- a/packages/react-widgets/src/index.d.ts +++ b/packages/react-widgets/src/index.d.ts @@ -25,6 +25,6 @@ export { default as BarWidget } from './widgets/BarWidget'; export { default as useSourceFilters } from './hooks/useSourceFilters'; export { default as FeatureSelectionWidget } from './widgets/FeatureSelectionWidget'; export { default as FeatureSelectionLayer } from './layers/FeatureSelectionLayer'; -export { default as useGeocoderWidgetController } from './hooks/useGeocoderWidgetController'; +export { default as useGeocoderWidgetController, setGeocoderResult } from './hooks/useGeocoderWidgetController'; export { WidgetState, WidgetStateType } from './types'; export { isRemoteCalculationSupported as _isRemoteCalculationSupported, sourceAndFiltersToSQL as _sourceAndFiltersToSQL } from './models/utils'; diff --git a/packages/react-widgets/src/index.js b/packages/react-widgets/src/index.js index ec25b3cc1..2ea2d0902 100644 --- a/packages/react-widgets/src/index.js +++ b/packages/react-widgets/src/index.js @@ -21,7 +21,10 @@ export { } from './models'; export { default as useSourceFilters } from './hooks/useSourceFilters'; export { default as FeatureSelectionLayer } from './layers/FeatureSelectionLayer'; -export { default as useGeocoderWidgetController } from './hooks/useGeocoderWidgetController'; +export { + default as useGeocoderWidgetController, + setGeocoderResult +} from './hooks/useGeocoderWidgetController'; export { WidgetStateType } from './hooks/useWidgetFetch'; export { isRemoteCalculationSupported as _isRemoteCalculationSupported, diff --git a/packages/react-widgets/src/layers/FeatureSelectionLayer.js b/packages/react-widgets/src/layers/FeatureSelectionLayer.js index fb4c42378..cd65569d5 100644 --- a/packages/react-widgets/src/layers/FeatureSelectionLayer.js +++ b/packages/react-widgets/src/layers/FeatureSelectionLayer.js @@ -12,6 +12,7 @@ import { hexToRgb, useTheme } from '@mui/material'; import EditableCartoGeoJsonLayer from './EditableCartoGeoJsonLayer'; import useEventManager from './useEventManager'; import MaskLayer from './MaskLayer'; +import kinks from '@turf/kinks'; const { ViewMode, TranslateMode, ModifyMode, CompositeMode } = nebulaModes; @@ -58,8 +59,15 @@ export default function FeatureSelectionLayer( const primaryAsRgba = formatRGBA(hexToRgb(theme.palette.primary.main)); const secondaryAsRgba = formatRGBA(hexToRgb(theme.palette.secondary.main)); + const errorAsRgba = formatRGBA(hexToRgb(theme.palette.error.main)); - const mainColor = hasGeometry && !isSelected ? secondaryAsRgba : primaryAsRgba; + let mainColor = primaryAsRgba; + if (hasGeometry && !isSelected) { + mainColor = secondaryAsRgba; + } + if (spatialFilterGeometry?.properties?.invalid) { + mainColor = errorAsRgba; + } return [ mask && MaskLayer(), @@ -92,10 +100,18 @@ export default function FeatureSelectionLayer( // 2. editType includes tentative, that means it's being drawn if (updatedData.features.length !== 0 && !editType.includes('Tentative')) { const [lastFeature] = updatedData.features.slice(-1); + const intersectionPoints = kinks(lastFeature).features.length; + if (lastFeature) { dispatch( addSpatialFilter({ - geometry: lastFeature + geometry: { + ...lastFeature, + properties: { + ...lastFeature.properties, + invalid: intersectionPoints > 0 + } + } }) ); } diff --git a/packages/react-widgets/src/layers/MaskLayer.js b/packages/react-widgets/src/layers/MaskLayer.js index b8d874174..6883c0457 100644 --- a/packages/react-widgets/src/layers/MaskLayer.js +++ b/packages/react-widgets/src/layers/MaskLayer.js @@ -1,10 +1,10 @@ import { useSelector } from 'react-redux'; import { SolidPolygonLayer } from '@deck.gl/layers/typed'; import { MASK_ID } from '@carto/react-core/'; -import { selectSpatialFilter } from '@carto/react-redux/'; +import { selectValidSpatialFilter } from '@carto/react-redux/'; export default function MaskLayer() { - const spatialFilterGeometry = useSelector((state) => selectSpatialFilter(state)); + const spatialFilterGeometry = useSelector(selectValidSpatialFilter); const maskData = !!spatialFilterGeometry ? [{ polygon: spatialFilterGeometry?.geometry?.coordinates }] : []; diff --git a/packages/react-workers/package.json b/packages/react-workers/package.json index 463f2d78b..22d948c8f 100644 --- a/packages/react-workers/package.json +++ b/packages/react-workers/package.json @@ -1,6 +1,6 @@ { "name": "@carto/react-workers", - "version": "2.3.10", + "version": "2.4.2", "description": "CARTO for React - Workers", "author": "CARTO Dev Team", "keywords": [ @@ -66,7 +66,7 @@ }, "dependencies": { "@babel/runtime": "^7.13.9", - "@carto/react-core": "^2.3.10", + "@carto/react-core": "^2.4.2", "@turf/bbox-polygon": "^6.3.0", "@turf/boolean-intersects": "^6.3.0", "@turf/boolean-within": "^6.3.0", diff --git a/yarn.lock b/yarn.lock index 6b2b91a43..766267c0a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4307,6 +4307,13 @@ dependencies: "@turf/helpers" "^6.5.0" +"@turf/kinks@^6.3.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@turf/kinks/-/kinks-6.5.0.tgz#80e7456367535365012f658cf1a988b39a2c920b" + integrity sha512-ViCngdPt1eEL7hYUHR2eHR662GvCgTc35ZJFaNR6kRtr6D8plLaDju0FILeFFWSc+o8e3fwxZEJKmFj9IzPiIQ== + dependencies: + "@turf/helpers" "^6.5.0" + "@turf/line-intersect@>=4.0.0", "@turf/line-intersect@^6.5.0": version "6.5.0" resolved "https://registry.yarnpkg.com/@turf/line-intersect/-/line-intersect-6.5.0.tgz#dea48348b30c093715d2195d2dd7524aee4cf020"