Skip to content

Commit b607877

Browse files
authored
Dynamic filter UI/UX Review geosolutions-it#11963 (geosolutions-it#11969)
1 parent 4b8eefb commit b607877

26 files changed

Lines changed: 557 additions & 81 deletions

web/client/components/misc/switch/__tests__/SwitchButton-test.jsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ describe('SwitchButton component', () => {
2929
const el = container.querySelector('.mapstore-switch-btn');
3030
expect(el).toExist();
3131
});
32+
it('applies custom className', () => {
33+
ReactDOM.render(<SwitchButton className="mapstore-switch-btn-xs" />, document.getElementById("container"));
34+
const container = document.getElementById('container');
35+
const el = container.querySelector('.mapstore-switch-btn.mapstore-switch-btn-xs');
36+
expect(el).toExist();
37+
});
3238
it('Test SwitchButton onChange', () => {
3339
const actions = {
3440
onChange: () => {}

web/client/components/widgets/builder/wizard/FilterWizard.jsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ const FilterWizard = ({
125125
key={`ms-filter-tab-${tab.id}`}
126126
eventKey={tab.id}
127127
onClick={() => setActiveTab(tab.id)}
128+
className="ms-filter-tab-item"
128129
>
129130
<span>{<Message msgId={tab.labelKey} />}</span>
130131
</NavItem>

web/client/components/widgets/builder/wizard/common/interactions/InteractionEventsSelector.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ const InteractionEventsSelector = ({target, expanded, toggleExpanded = () => {},
4242
}
4343
</Button>
4444
<Glyphicon glyph={target?.glyph} />
45-
<Text className="ms-flex-fill" fontSize="md"><Message msgId={targetTitleTranslationMap[target.title] || ""} /></Text>
45+
<Text className="ms-flex-fill"><Message msgId={targetTitleTranslationMap[target.title] || ""} /></Text>
4646

4747

4848
</FlexBox>

web/client/components/widgets/builder/wizard/common/interactions/InteractionsRow.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ const InteractionsRow = ({item, target, interactions, sourceWidgetId, interactio
122122
</Button>
123123
)}
124124
<Glyphicon glyph={item.icon}/>
125-
<Text className="ms-flex-fill">{itemTitleTranslationMap[item.title] ? <Message msgId={itemTitleTranslationMap[item.title] } /> : <LocalizedString value={item.title}/> }</Text>
125+
<Text className="ms-flex-fill ">{itemTitleTranslationMap[item.title] ? <Message msgId={itemTitleTranslationMap[item.title] } /> : <LocalizedString value={item.title}/> }</Text>
126126
{item.interactionMetadata && item.type === "element" && (
127127
<InteractionButtons
128128
item={item}

web/client/components/widgets/builder/wizard/filter/FilterDropdownList.jsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@ import React, { useMemo } from 'react';
99
import PropTypes from 'prop-types';
1010
import { FormGroup } from 'react-bootstrap';
1111
import Select from 'react-select';
12+
import './FilterDropdownList.less';
1213

1314
const FilterDropdownList = ({
1415
items = [],
1516
selectionMode = 'multiple',
1617
selectedValues,
1718
placeholder = 'Select...',
18-
onSelectionChange = () => {}
19+
onSelectionChange = () => {},
20+
layoutMaxHeight
1921
}) => {
2022
const isSingle = selectionMode === 'single';
2123
const normalizedValues = Array.isArray(selectedValues)
@@ -46,10 +48,16 @@ const FilterDropdownList = ({
4648
onSelectionChange(nextValues);
4749
};
4850

51+
const hasScrollableValues = typeof layoutMaxHeight === 'number' && normalizedValues.length > 0;
52+
const valueContainerStyle = hasScrollableValues
53+
? { ['--filter-widget-dropdown-value-max-height']: `${layoutMaxHeight}px` }
54+
: undefined;
55+
const listClassName = ['ms-filter-widget-dropdown-list', hasScrollableValues && 'ms-filter-widget-dropdown-list--scrollable-values'].filter(Boolean).join(' ');
56+
4957
return (
50-
<FormGroup className="ms-filter-dropdown-list">
58+
<FormGroup className={listClassName} style={valueContainerStyle}>
5159
<Select
52-
className="ms-filter-dropdown"
60+
className="ms-filter-widget-dropdown"
5361
clearable={isSingle}
5462
multi={!isSingle}
5563
closeOnSelect={isSingle}
@@ -73,7 +81,8 @@ FilterDropdownList.propTypes = {
7381
PropTypes.oneOfType([PropTypes.string, PropTypes.number])
7482
),
7583
placeholder: PropTypes.string,
76-
onSelectionChange: PropTypes.func
84+
onSelectionChange: PropTypes.func,
85+
layoutMaxHeight: PropTypes.number
7786
};
7887

7988
export default FilterDropdownList;
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/*
2+
* Copyright 2026, GeoSolutions.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
/* Value container: selected tags area - max height + scroll only when there are selected values */
10+
.ms-filter-widget-dropdown-list {
11+
&.ms-filter-widget-dropdown-list--scrollable-values .Select-multi-value-wrapper {
12+
max-height: var(--filter-widget-dropdown-value-max-height);
13+
overflow-y: auto;
14+
}
15+
}

web/client/components/widgets/builder/wizard/filter/FilterLayoutTab.jsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ const FilterLayoutTab = ({
6666
<FlexBox style={{ width: '100%' }}>
6767
<div
6868
onClick={() => handlePanelToggle('title')}
69-
style={{ cursor: 'pointer' }}
69+
className="accordion-title"
7070
>
7171
<Glyphicon glyph={expandedPanel === 'title' ? 'bottom' : 'next'} style={{ marginRight: 8 }} />
7272
<strong style={{ color: 'inherit' }}><Message msgId="widgets.filterWidget.title" /></strong>
@@ -76,6 +76,7 @@ const FilterLayoutTab = ({
7676
<SwitchButton
7777
checked={!layout.titleDisabled}
7878
onChange={(checked) => onChange('layout.titleDisabled', !checked)}
79+
className="mapstore-switch-btn-xs"
7980
/>
8081
</div>
8182
</FlexBox>
@@ -171,7 +172,7 @@ const FilterLayoutTab = ({
171172
header={
172173
<div
173174
onClick={() => handlePanelToggle('items')}
174-
style={{ cursor: 'pointer' }}
175+
className="accordion-title"
175176
>
176177
<Glyphicon glyph={expandedPanel === 'items' ? 'bottom' : 'next'} style={{ marginRight: 8 }} />
177178
<strong ><Message msgId="widgets.filterWidget.items" /></strong>

web/client/components/widgets/builder/wizard/filter/FilterSwitchList.jsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ const FilterSwitchList = ({
6161
checked={isChecked}
6262
disabled={disabled}
6363
onChange={() => handleToggle(id, isChecked)}
64+
className="mapstore-switch-btn-xs"
6465
/>
6566
<span className="ms-filter-switch-list-item-label">{label}</span>
6667
</div>
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/*
2+
* Copyright 2026, GeoSolutions
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
import expect from 'expect';
9+
import React from 'react';
10+
import ReactDOM from 'react-dom';
11+
import FilterDropdownList from '../FilterDropdownList';
12+
13+
describe('FilterDropdownList', () => {
14+
beforeEach((done) => {
15+
document.body.innerHTML = '<div id="container"></div>';
16+
setTimeout(done);
17+
});
18+
19+
afterEach((done) => {
20+
ReactDOM.unmountComponentAtNode(document.getElementById("container"));
21+
document.body.innerHTML = '';
22+
setTimeout(done);
23+
});
24+
25+
it('should apply scrollable values styles only when there are selected values', () => {
26+
const container = document.getElementById("container");
27+
const items = [
28+
{ id: '1', label: 'Alabama' },
29+
{ id: '2', label: 'Arizona' }
30+
];
31+
32+
// empty selection => no modifier class and no CSS variable
33+
ReactDOM.render(
34+
<FilterDropdownList
35+
items={items}
36+
selectionMode="multiple"
37+
selectedValues={[]}
38+
layoutMaxHeight={100}
39+
onSelectionChange={() => {}}
40+
/>,
41+
container
42+
);
43+
const listNodeEmpty = container.querySelector('.ms-filter-widget-dropdown-list');
44+
expect(listNodeEmpty).toExist();
45+
expect(listNodeEmpty.className).toNotContain('ms-filter-widget-dropdown-list--scrollable-values');
46+
expect(listNodeEmpty.style.getPropertyValue('--filter-widget-dropdown-value-max-height')).toBe('');
47+
48+
// with selection => modifier class and CSS variable set
49+
ReactDOM.render(
50+
<FilterDropdownList
51+
items={items}
52+
selectionMode="multiple"
53+
selectedValues={['1']}
54+
layoutMaxHeight={100}
55+
onSelectionChange={() => {}}
56+
/>,
57+
container
58+
);
59+
const listNodeSelected = container.querySelector('.ms-filter-widget-dropdown-list');
60+
expect(listNodeSelected).toExist();
61+
expect(listNodeSelected.className).toContain('ms-filter-widget-dropdown-list--scrollable-values');
62+
expect(listNodeSelected.style.getPropertyValue('--filter-widget-dropdown-value-max-height')).toBe('100px');
63+
// sanity: react-select wrapper uses passed className + multi modifier
64+
expect(container.querySelector('.ms-filter-widget-dropdown.Select--multi')).toExist();
65+
});
66+
67+
it('should not apply scrollable styles when layoutMaxHeight is missing', () => {
68+
const container = document.getElementById("container");
69+
const items = [
70+
{ id: '1', label: 'Alabama' },
71+
{ id: '2', label: 'Arizona' }
72+
];
73+
74+
ReactDOM.render(
75+
<FilterDropdownList
76+
items={items}
77+
selectionMode="multiple"
78+
selectedValues={['1']}
79+
onSelectionChange={() => {}}
80+
/>,
81+
container
82+
);
83+
84+
const listNode = container.querySelector('.ms-filter-widget-dropdown-list');
85+
expect(listNode).toExist();
86+
expect(listNode.className).toNotContain('ms-filter-widget-dropdown-list--scrollable-values');
87+
expect(listNode.style.getPropertyValue('--filter-widget-dropdown-value-max-height')).toBe('');
88+
expect(container.querySelector('.ms-filter-widget-dropdown.Select--multi')).toExist();
89+
});
90+
});
91+

web/client/components/widgets/builder/wizard/filter/__tests__/FilterLayoutTab-test.jsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ describe('FilterLayoutTab component', () => {
5252
const titleSpan = titlePanelHeader.querySelector('span');
5353
expect(titleSpan).toExist();
5454

55-
const titleToggle = titlePanelHeader.querySelector('div[style*="cursor: pointer"]');
55+
const titleToggle = titlePanelHeader.querySelector('.accordion-title');
56+
expect(titleToggle).toExist();
5657
Simulate.click(titleToggle);
5758

5859
// Verify there are 6 input groups in title panel

0 commit comments

Comments
 (0)