@opengeoweb/time-series 14.1.0 → 14.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.esm.js CHANGED
@@ -4,7 +4,7 @@ import React__default, { useState, useMemo, useEffect, useCallback, memo } from
4
4
  import { useDispatch, useSelector } from 'react-redux';
5
5
  import { mapUtils, selectorMemoizationOptions, genericSelectors, uiSelectors, uiTypes, uiActions, useSetupDialog, mapSelectors, mapListener, serviceListener, layersListener, syncGroupsListener, genericListener, openlayersListener, uiReducer, syncGroupsReducer, layerReducer, serviceReducer, mapReducer, loadingIndicatorActions, syncGroupsActions, genericActions, mapActions, layerActions } from '@opengeoweb/store';
6
6
  import { snackbarActions, snackbarTypes, snackbarListener, snackbarReducer } from '@opengeoweb/snackbar';
7
- import { dateUtils, CustomTooltip, CustomToggleButton, TooltipSelect, CustomIconButton, CustomAccordion, ToolContainerDraggable, SearchHighlight, SearchField, ToggleMenu, CustomDialog, AlertBanner, useConfirmationDialog, SwitchButton } from '@opengeoweb/shared';
7
+ import { dateUtils, CustomTooltip, CustomToggleButton, TooltipSelect, CustomIconButton, CustomAccordion, ToolContainerDraggable, SearchHighlight, SearchField, ToggleMenu, CustomDialog, AlertBanner, useConfirmationDialog, SwitchButton, useResizeObserver } from '@opengeoweb/shared';
8
8
  import { MenuItem, useTheme, LinearProgress, Grid, Box, Typography, ClickAwayListener, Input, Collapse, ListItem, ListItemButton, ListItemText, List, Button, InputAdornment, Card, styled, Stack, Tooltip, ListItemIcon, Checkbox } from '@mui/material';
9
9
  import { isString, trim, trimStart, compact, chain, groupBy, sortBy, uniqBy, isEqual } from 'lodash';
10
10
  import { ReactSortable } from 'react-sortablejs';
@@ -7830,7 +7830,26 @@ var LocationListItem = function LocationListItem(_ref) {
7830
7830
  var theme = useTheme();
7831
7831
  var _useTimeseriesTransla = useTimeseriesTranslation();
7832
7832
  _useTimeseriesTransla.t;
7833
+ // Wrapper ref used for hover detection AND restoring keyboard focus after selection.
7833
7834
  var mouseMoveRef = React__default.useRef(null);
7835
+ // Listen for global selection events so we can re-focus the item that was activated via keyboard.
7836
+ React__default.useEffect(function () {
7837
+ var handleSelection = function handleSelection(e) {
7838
+ var _custom$detail;
7839
+ var custom = e;
7840
+ if (((_custom$detail = custom.detail) == null ? void 0 : _custom$detail.location) === location && mouseMoveRef.current) {
7841
+ // Delay to allow parent state updates / virtualization re-render.
7842
+ requestAnimationFrame(function () {
7843
+ var _mouseMoveRef$current;
7844
+ (_mouseMoveRef$current = mouseMoveRef.current) == null || _mouseMoveRef$current.focus();
7845
+ });
7846
+ }
7847
+ };
7848
+ window.addEventListener('location-list-selection', handleSelection);
7849
+ return function () {
7850
+ window.removeEventListener('location-list-selection', handleSelection);
7851
+ };
7852
+ }, [location]);
7834
7853
  var textContainerRef = React__default.useRef(null);
7835
7854
  var _React$useState = React__default.useState(false),
7836
7855
  isHovering = _React$useState[0],
@@ -7866,6 +7885,23 @@ var LocationListItem = function LocationListItem(_ref) {
7866
7885
  setTextContainerWidth(textContainerRef.current.clientWidth);
7867
7886
  }
7868
7887
  }, [textContainerRef]);
7888
+ // If selection state changes and this item is now the selected one, ensure it regains focus.
7889
+ React__default.useEffect(function () {
7890
+ if (selectedLocation === location && mouseMoveRef.current) {
7891
+ // If focus already on this item, do nothing.
7892
+ if (mouseMoveRef.current === document.activeElement) {
7893
+ return;
7894
+ }
7895
+ // If focus is outside the list item, re-focus after render settles.
7896
+ // Use double rAF for virtualization/layout stability.
7897
+ requestAnimationFrame(function () {
7898
+ requestAnimationFrame(function () {
7899
+ var _mouseMoveRef$current2;
7900
+ (_mouseMoveRef$current2 = mouseMoveRef.current) == null || _mouseMoveRef$current2.focus();
7901
+ });
7902
+ });
7903
+ }
7904
+ }, [selectedLocation, location]);
7869
7905
  return jsx(OverflowTooltip, {
7870
7906
  text: location,
7871
7907
  textContainerWidth: textContainerWidth,
@@ -7875,6 +7911,29 @@ var LocationListItem = function LocationListItem(_ref) {
7875
7911
  onMouseLeave: handleMouseLeave,
7876
7912
  onFocus: handleMouseEnter,
7877
7913
  onBlur: handleMouseLeave,
7914
+ tabIndex: 0,
7915
+ role: "option",
7916
+ "aria-selected": selectedLocation === location,
7917
+ onKeyDown: function onKeyDown(e) {
7918
+ if (isDisabled) {
7919
+ return;
7920
+ }
7921
+ if (e.key === 'Enter' || e.key === ' ') {
7922
+ e.preventDefault();
7923
+ handleChange(location);
7924
+ // Dispatch custom event to remove focus styling from outer list container
7925
+ window.dispatchEvent(new CustomEvent('location-list-selection', {
7926
+ detail: {
7927
+ location: location
7928
+ }
7929
+ }));
7930
+ // Explicitly retain focus immediately (backup before effect runs)
7931
+ requestAnimationFrame(function () {
7932
+ var _mouseMoveRef$current3;
7933
+ (_mouseMoveRef$current3 = mouseMoveRef.current) == null || _mouseMoveRef$current3.focus();
7934
+ });
7935
+ }
7936
+ },
7878
7937
  children: jsxs(ListItem, Object.assign({
7879
7938
  "data-testid": "locationDropdownListItem",
7880
7939
  role: "listitem"
@@ -7909,7 +7968,19 @@ var LocationListItem = function LocationListItem(_ref) {
7909
7968
  backgroundColor: 'transparent'
7910
7969
  }
7911
7970
  },
7912
- disabled: isDisabled
7971
+ disabled: isDisabled,
7972
+ // Remove from tab order so row wrapper is the single focus target
7973
+ tabIndex: -1,
7974
+ onChange: function onChange() {
7975
+ if (!isDisabled) {
7976
+ handleChange(location);
7977
+ window.dispatchEvent(new CustomEvent('location-list-selection', {
7978
+ detail: {
7979
+ location: location
7980
+ }
7981
+ }));
7982
+ }
7983
+ }
7913
7984
  })
7914
7985
  }), jsx(Typography, {
7915
7986
  ref: textContainerRef,
@@ -7949,6 +8020,121 @@ var renderRow = function renderRow(features, handleChange, selectedLocation, isD
7949
8020
  };
7950
8021
  };
7951
8022
 
8023
+ var FocusableListOuter = /*#__PURE__*/React.forwardRef(function (props, ref) {
8024
+ // Track last interaction modality to conditionally show focus ring only for keyboard navigation.
8025
+ var lastInteractionWasKeyboard = React.useRef(false);
8026
+ React.useEffect(function () {
8027
+ var handleKey = function handleKey(e) {
8028
+ // Ignore modifier-only keys; any navigation key sets keyboard modality.
8029
+ if (e.key) {
8030
+ lastInteractionWasKeyboard.current = true;
8031
+ }
8032
+ };
8033
+ var handlePointer = function handlePointer() {
8034
+ lastInteractionWasKeyboard.current = false;
8035
+ };
8036
+ window.addEventListener('keydown', handleKey, {
8037
+ capture: true
8038
+ });
8039
+ window.addEventListener('mousedown', handlePointer, {
8040
+ capture: true
8041
+ });
8042
+ window.addEventListener('pointerdown', handlePointer, {
8043
+ capture: true
8044
+ });
8045
+ window.addEventListener('touchstart', handlePointer, {
8046
+ capture: true
8047
+ });
8048
+ // Listen for custom selection event to remove focus styling after keyboard activation.
8049
+ var handleSelection = function handleSelection() {
8050
+ setIsFocusVisible(false);
8051
+ };
8052
+ window.addEventListener('location-list-selection', handleSelection);
8053
+ return function () {
8054
+ window.removeEventListener('keydown', handleKey, {
8055
+ capture: true
8056
+ });
8057
+ window.removeEventListener('mousedown', handlePointer, {
8058
+ capture: true
8059
+ });
8060
+ window.removeEventListener('pointerdown', handlePointer, {
8061
+ capture: true
8062
+ });
8063
+ window.removeEventListener('touchstart', handlePointer, {
8064
+ capture: true
8065
+ });
8066
+ window.removeEventListener('location-list-selection', handleSelection);
8067
+ };
8068
+ }, []);
8069
+ var handleKeyDown = function handleKeyDown(e) {
8070
+ var el = ref && typeof ref === 'object' && 'current' in ref ? ref.current : null;
8071
+ if (!el) {
8072
+ return;
8073
+ }
8074
+ var lineHeight = 40; // matches itemSize
8075
+ var pageHeight = el.clientHeight;
8076
+ switch (e.key) {
8077
+ case 'ArrowDown':
8078
+ e.preventDefault();
8079
+ el.scrollTop += lineHeight;
8080
+ break;
8081
+ case 'ArrowUp':
8082
+ e.preventDefault();
8083
+ el.scrollTop -= lineHeight;
8084
+ break;
8085
+ case 'PageDown':
8086
+ e.preventDefault();
8087
+ el.scrollTop += pageHeight;
8088
+ break;
8089
+ case 'PageUp':
8090
+ e.preventDefault();
8091
+ el.scrollTop -= pageHeight;
8092
+ break;
8093
+ case 'Home':
8094
+ e.preventDefault();
8095
+ el.scrollTop = 0;
8096
+ break;
8097
+ case 'End':
8098
+ e.preventDefault();
8099
+ el.scrollTop = el.scrollHeight;
8100
+ break;
8101
+ }
8102
+ };
8103
+ // merge any existing style prop with our outline override
8104
+ var mergedStyle = Object.assign({}, props.style, {
8105
+ outline: 'none',
8106
+ transition: 'box-shadow 120ms ease, background-color 120ms ease'
8107
+ });
8108
+ var _React$useState = React.useState(false),
8109
+ isFocusVisible = _React$useState[0],
8110
+ setIsFocusVisible = _React$useState[1];
8111
+ var onFocus = function onFocus() {
8112
+ if (lastInteractionWasKeyboard.current) {
8113
+ setIsFocusVisible(true);
8114
+ }
8115
+ };
8116
+ var onBlur = function onBlur() {
8117
+ return setIsFocusVisible(false);
8118
+ };
8119
+ if (isFocusVisible) {
8120
+ mergedStyle.boxShadow = '0 0 0 2px var(--focus-ring-color, #1976d2)';
8121
+ mergedStyle.backgroundColor = 'rgba(25,118,210,0.06)';
8122
+ }
8123
+ return jsx("div", Object.assign({}, props, {
8124
+ ref: ref,
8125
+ tabIndex: 0,
8126
+ role: "listbox",
8127
+ "aria-label": "Locations list",
8128
+ onKeyDown: handleKeyDown,
8129
+ onFocus: onFocus,
8130
+ onBlur: onBlur,
8131
+ // Remove focus styling immediately on mouse interaction
8132
+ onMouseDown: function onMouseDown() {
8133
+ setIsFocusVisible(false);
8134
+ },
8135
+ style: mergedStyle
8136
+ }));
8137
+ });
7952
8138
  var LocationDropdown = function LocationDropdown(_ref) {
7953
8139
  var _combinedSelectedLoca;
7954
8140
  var geoJson = _ref.geoJson,
@@ -7956,10 +8142,11 @@ var LocationDropdown = function LocationDropdown(_ref) {
7956
8142
  handleChange = _ref.handleChange,
7957
8143
  selectedLocation = _ref.selectedLocation,
7958
8144
  _ref$isDisabled = _ref.isDisabled,
7959
- isDisabled = _ref$isDisabled === void 0 ? false : _ref$isDisabled;
7960
- var _React$useState = React.useState(''),
7961
- searchTerm = _React$useState[0],
7962
- setSearchTerm = _React$useState[1];
8145
+ isDisabled = _ref$isDisabled === void 0 ? false : _ref$isDisabled,
8146
+ defaultExpanded = _ref.defaultExpanded;
8147
+ var _React$useState2 = React.useState(''),
8148
+ searchTerm = _React$useState2[0],
8149
+ setSearchTerm = _React$useState2[1];
7963
8150
  var combinedSelectedLocation = geoJson == null ? void 0 : geoJson.features.find(function (feature) {
7964
8151
  var _feature$properties;
7965
8152
  return selectedLocation === ((_feature$properties = feature.properties) == null ? void 0 : _feature$properties.name);
@@ -7978,8 +8165,11 @@ var LocationDropdown = function LocationDropdown(_ref) {
7978
8165
  });
7979
8166
  });
7980
8167
  }, [searchTerm, splitGeoJson]);
8168
+ var listContainerRef = React.useRef(null);
8169
+ var heightObserver = useResizeObserver(listContainerRef, filteredResults);
7981
8170
  return jsx(Box, {
7982
8171
  "data-testid": "locationDropdownContainer",
8172
+ ref: listContainerRef,
7983
8173
  sx: {
7984
8174
  width: '100%',
7985
8175
  height: '100%',
@@ -8023,16 +8213,17 @@ var LocationDropdown = function LocationDropdown(_ref) {
8023
8213
  title: collectionName,
8024
8214
  title2: serviceName,
8025
8215
  amountOfItems: collectionGeoJson.features.length || 0,
8026
- defaultExpanded: false,
8216
+ defaultExpanded: defaultExpanded || false,
8027
8217
  sx: {
8028
8218
  mx: 1,
8029
8219
  mb: 1
8030
8220
  },
8031
8221
  children: jsx(FixedSizeList, {
8032
- height: 350,
8222
+ height: heightObserver,
8033
8223
  width: "100%",
8034
8224
  itemSize: 40,
8035
8225
  itemCount: collectionGeoJson.features.length,
8226
+ outerElementType: FocusableListOuter,
8036
8227
  children: renderRow(collectionGeoJson.features, handleChange, collectionSelectedLocation == null || (_collectionSelectedLo = collectionSelectedLocation.properties) == null ? void 0 : _collectionSelectedLo.name, isDisabled)
8037
8228
  })
8038
8229
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeoweb/time-series",
3
- "version": "14.1.0",
3
+ "version": "14.2.2",
4
4
  "description": "GeoWeb time-series library for the opengeoweb project",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -9,26 +9,26 @@
9
9
  },
10
10
  "dependencies": {
11
11
  "immer": "^10.0.3",
12
- "@opengeoweb/shared": "14.1.0",
12
+ "@opengeoweb/shared": "14.2.2",
13
13
  "@reduxjs/toolkit": "^2.6.1",
14
14
  "react-redux": "^9.2.0",
15
- "@opengeoweb/webmap": "14.1.0",
16
- "@opengeoweb/webmap-react": "14.1.0",
15
+ "@opengeoweb/webmap": "14.2.2",
16
+ "@opengeoweb/webmap-react": "14.2.2",
17
17
  "lodash": "^4.17.21",
18
- "@opengeoweb/api": "14.1.0",
19
- "@opengeoweb/theme": "14.1.0",
20
- "@opengeoweb/store": "14.1.0",
18
+ "@opengeoweb/api": "14.2.2",
19
+ "@opengeoweb/theme": "14.2.2",
20
+ "@opengeoweb/store": "14.2.2",
21
21
  "echarts-for-react": "^3.0.2",
22
22
  "echarts": "^5.4.3",
23
23
  "react-sortablejs": "^6.1.4",
24
- "@opengeoweb/snackbar": "14.1.0",
24
+ "@opengeoweb/snackbar": "14.2.2",
25
25
  "react-window": "^1.8.10",
26
26
  "react-virtualized-auto-sizer": "^1.0.21",
27
27
  "msw": "^2.7.3",
28
28
  "i18next": "^25.0.1",
29
29
  "react-i18next": "^15.1.1",
30
- "@opengeoweb/core": "14.1.0",
31
- "@opengeoweb/form-fields": "14.1.0",
30
+ "@opengeoweb/core": "14.2.2",
31
+ "@opengeoweb/form-fields": "14.2.2",
32
32
  "@mui/material": "^7.0.1",
33
33
  "react-hook-form": "^7.61.1",
34
34
  "uuid": "^10.0.0",
@@ -7,6 +7,7 @@ interface LocationDropdownProps {
7
7
  selectedLocation: string;
8
8
  geoJson?: FeatureCollection<GeometryObject>;
9
9
  isDisabled?: boolean;
10
+ defaultExpanded?: boolean;
10
11
  }
11
12
  export declare const LocationDropdown: React.FC<LocationDropdownProps>;
12
13
  export {};
@@ -6,4 +6,6 @@ type Story = StoryObj<typeof LocationDropdown>;
6
6
  export declare const Component: Story;
7
7
  export declare const ComponentDark: Story;
8
8
  export declare const LocationDropdownDemoLight: Story;
9
+ export declare const LocationDropdownExpandedDemoLight: Story;
9
10
  export declare const LocationDropdownDemoDark: Story;
11
+ export declare const LocationDropdownExpandedDemoDark: Story;