Skip to content

Commit 5546230

Browse files
Vojtěch Václav Portešvojtechportes
authored andcommitted
feat: Improved responsivness of Builder and related components
1 parent 399dde8 commit 5546230

19 files changed

Lines changed: 454 additions & 252 deletions

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,3 +103,17 @@ Supported formats are documented on the website:
103103
- [Supported Formats](https://vojtechportes.github.io/react-query-builder/documentation/parsing-and-formatting/supported-formats)
104104
- [formatQuery API](https://vojtechportes.github.io/react-query-builder/api/format-query)
105105
- [parseQuery API](https://vojtechportes.github.io/react-query-builder/api/parse-query)
106+
107+
## Responsive Behavior
108+
109+
The default builder components include a compact responsive layout for medium-width screens.
110+
111+
- Rule rows reflow to preserve field, operator, action, and value legibility when horizontal space gets tighter.
112+
- Multi-select values use a summarized closed state to avoid chip overflow.
113+
- The default responsive behavior applies automatically when you use the built-in components.
114+
- If you replace layout containers such as `components.Rule` or `components.Group`, your custom components are responsible for their own responsive behavior.
115+
116+
Responsive behavior is documented in more detail on the website:
117+
118+
- [Components](https://vojtechportes.github.io/react-query-builder/documentation/components)
119+
- [API: Components](https://vojtechportes.github.io/react-query-builder/api/components)

example/src/components/demo-playground.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,6 @@ const BuilderCard = styled.section`
8080
`;
8181

8282
const BuilderSurface = styled.div`
83-
min-width: 920px;
8483
font-family: Arial, sans-serif;
8584
font-size: 16px;
8685
line-height: normal;

example/src/components/parsing-sandbox.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,6 @@ const BuilderScrollArea = styled.div`
9898

9999
const BuilderViewport = styled.div`
100100
display: inline-block;
101-
min-width: 920px;
102101
font-family: Arial, sans-serif;
103102
font-size: 16px;
104103
line-height: normal;

example/src/pages/documentation-page/pages/documentation-content.tsx

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -655,7 +655,7 @@ export const documentationPages: IDocumentationPage[] = [
655655
description:
656656
'Documentation for replacing built-in builder controls and containers with custom components.',
657657
searchText:
658-
'Components component overrides custom controls custom renderers builder customization add remove select input group rule',
658+
'Components component overrides custom controls custom renderers builder customization add remove select input group rule responsive responsiveness compact layout multiselect summary',
659659
content: (
660660
<>
661661
<p>
@@ -686,6 +686,26 @@ export const documentationPages: IDocumentationPage[] = [
686686
semantics and remain accessible.
687687
</li>
688688
</List>
689+
<SectionTitle>Responsive behavior</SectionTitle>
690+
<List>
691+
<li>
692+
The built-in <InlineCode>Rule</InlineCode> and <InlineCode>Group</InlineCode>{' '}
693+
components include a compact responsive layout for medium-width screens.
694+
</li>
695+
<li>
696+
Multi-select controls use a summarized closed state so selected
697+
values do not overflow the available rule width.
698+
</li>
699+
<li>
700+
Responsive behavior is automatic when you use the default
701+
components.
702+
</li>
703+
<li>
704+
If you replace <InlineCode>components.Rule</InlineCode> or{' '}
705+
<InlineCode>components.Group</InlineCode>, your custom layout is
706+
responsible for its own responsive behavior.
707+
</li>
708+
</List>
689709
<AlertBox title="API reference" variant="info">
690710
<TextLink to="/api/components">Components</TextLink>.
691711
</AlertBox>

src/form/__snapshots__/select-multi.test.tsx.snap

Lines changed: 39 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -7,87 +7,47 @@ exports[`#components/SelectMulti Tests Snapshot 1`] = `
77
type="hidden"
88
value="test"
99
/>
10-
<styled.div>
11-
<Trigger
12-
disabled={false}
13-
expanded={false}
14-
label="Select value"
15-
onClick={[Function]}
16-
theme={
17-
{
18-
"colors": {
19-
"grey": {
20-
"100": "#f5f5f5",
21-
"200": "#eeeeee",
22-
"300": "#e0e0e0",
23-
"400": "#bdbdbd",
24-
"500": "#9e9e9e",
25-
"600": "#757575",
26-
"700": "#616161",
27-
"800": "#424242",
28-
"900": "#212121",
29-
},
30-
"primary": {
31-
"contrastText": "#ffffff",
32-
"dark": "#002984",
33-
"default": "#3f51b5",
34-
"light": "#757de8",
35-
},
36-
"secondary": {
37-
"contrastText": "#ffffff",
38-
"dark": "#ba000d",
39-
"default": "#f44336",
40-
"light": "#ff7961",
41-
},
42-
"white": "#ffffff",
10+
<Trigger
11+
disabled={false}
12+
expanded={false}
13+
label="test"
14+
onClick={[Function]}
15+
theme={
16+
{
17+
"colors": {
18+
"grey": {
19+
"100": "#f5f5f5",
20+
"200": "#eeeeee",
21+
"300": "#e0e0e0",
22+
"400": "#bdbdbd",
23+
"500": "#9e9e9e",
24+
"600": "#757575",
25+
"700": "#616161",
26+
"800": "#424242",
27+
"900": "#212121",
4328
},
44-
}
45-
}
46-
triggerRef={
47-
{
48-
"current": null,
49-
}
50-
}
51-
/>
52-
</styled.div>
53-
<styled.div>
54-
<Tag
55-
disabled={false}
56-
key="test"
57-
label="test"
58-
onRemove={[MockFunction]}
59-
theme={
60-
{
61-
"colors": {
62-
"grey": {
63-
"100": "#f5f5f5",
64-
"200": "#eeeeee",
65-
"300": "#e0e0e0",
66-
"400": "#bdbdbd",
67-
"500": "#9e9e9e",
68-
"600": "#757575",
69-
"700": "#616161",
70-
"800": "#424242",
71-
"900": "#212121",
72-
},
73-
"primary": {
74-
"contrastText": "#ffffff",
75-
"dark": "#002984",
76-
"default": "#3f51b5",
77-
"light": "#757de8",
78-
},
79-
"secondary": {
80-
"contrastText": "#ffffff",
81-
"dark": "#ba000d",
82-
"default": "#f44336",
83-
"light": "#ff7961",
84-
},
85-
"white": "#ffffff",
29+
"primary": {
30+
"contrastText": "#ffffff",
31+
"dark": "#002984",
32+
"default": "#3f51b5",
33+
"light": "#757de8",
34+
},
35+
"secondary": {
36+
"contrastText": "#ffffff",
37+
"dark": "#ba000d",
38+
"default": "#f44336",
39+
"light": "#ff7961",
8640
},
87-
}
41+
"white": "#ffffff",
42+
},
8843
}
89-
value="test"
90-
/>
91-
</styled.div>
44+
}
45+
title="test"
46+
triggerRef={
47+
{
48+
"current": null,
49+
}
50+
}
51+
/>
9252
</styled.div>
9353
`;

src/form/select-multi.test.tsx

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ import { SelectMulti } from './select-multi';
22
import { mount, shallow } from 'enzyme';
33
import React from 'react';
44

5-
const mockValues = [{ value: 'test', label: 'test' }];
5+
const mockValues = [
6+
{ value: 'test', label: 'test' },
7+
{ value: 'another', label: 'another' },
8+
];
69

710
describe('#components/SelectMulti', () => {
811
it('Tests Snapshot', () => {
@@ -39,27 +42,31 @@ describe('#components/SelectMulti', () => {
3942
const option = wrapper.find('[data-test="SelectMultiOption[test]"]').first();
4043
option.simulate('click');
4144

42-
const remove = wrapper.find('[data-test="Delete"]').first();
43-
remove.simulate('click');
45+
const secondOption = wrapper.find('[data-test="SelectMultiOption[another]"]').first();
46+
secondOption.simulate('click');
4447

45-
expect(onChange).toBeCalledTimes(0);
46-
expect(onDelete).toBeCalledTimes(2);
48+
expect(onChange).toBeCalledTimes(1);
49+
expect(onDelete).toBeCalledTimes(1);
4750
});
4851

49-
it('Does not render remove buttons when disabled', () => {
52+
it('Shows summary badge for hidden values', () => {
5053
const wrapper = mount(
5154
<SelectMulti
52-
disabled
55+
disabled={false}
5356
onChange={jest.fn()}
5457
onDelete={jest.fn()}
55-
selectedValue={['test']}
56-
values={mockValues}
58+
selectedValue={['test', 'another', 'third', 'fourth']}
59+
values={[
60+
{ value: 'test', label: 'Retail' },
61+
{ value: 'another', label: 'Priority' },
62+
{ value: 'third', label: 'Enterprise' },
63+
{ value: 'fourth', label: 'Wholesale' },
64+
]}
5765
/>
5866
);
5967

60-
const tags = wrapper.find('[data-test="SelectMultiTag"]').hostNodes();
61-
62-
expect(tags).toHaveLength(1);
63-
expect(tags.find('[data-test="Delete"]')).toHaveLength(0);
68+
expect(
69+
wrapper.find('[data-test="SelectMultiSummaryBadge"]').first().text()
70+
).toEqual('+1');
6471
});
6572
});

src/form/select-multi.tsx

Lines changed: 32 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -3,32 +3,17 @@ import styled from 'styled-components';
33
import { useTheme } from '../theme-provider/hooks/use-theme';
44
import { Trigger } from '../widgets/select-multi/components/trigger';
55
import { Option } from '../widgets/select-multi/components/option';
6-
import { Tag } from '../widgets/select-multi/components/tag';
76
import { useSelectMulti } from '../widgets/select-multi/hooks/use-select-multi';
87
import { getSelectedOptions } from '../widgets/select-multi/utils/get-selected-options.util';
8+
import { createSummary } from '../widgets/select-multi/utils/create-summary.util';
99
import { Popover } from './popover';
1010
import { ISelectProps } from './select';
1111

1212
const Container = styled.div`
13-
display: inline-grid;
14-
grid-template-columns: min-content max-content;
15-
align-items: center;
16-
gap: 0.35rem;
17-
width: max-content;
18-
max-width: 100%;
19-
`;
20-
21-
const TriggerContainer = styled.div`
2213
position: relative;
23-
flex: 0 0 auto;
24-
`;
25-
26-
const Tags = styled.div`
27-
display: flex;
28-
flex-wrap: wrap;
29-
gap: 0.35rem;
30-
align-items: center;
31-
align-self: center;
14+
display: inline-block;
15+
width: var(--query-builder-control-width, 160px);
16+
min-width: var(--query-builder-control-min-width, 160px);
3217
max-width: 100%;
3318
`;
3419

@@ -61,6 +46,9 @@ export const SelectMulti: FC<ISelectMultiProps> = ({
6146
disabled,
6247
});
6348
const selectedOptions = getSelectedOptions(values, selectedValue);
49+
const selectedLabels = selectedOptions.map(({ label }) => label);
50+
const summary = createSummary(selectedLabels);
51+
const title = summary.text ? selectedLabels.join(', ') : emptyValue || 'Select value';
6452

6553
const handleToggleValue = (value: string) => {
6654
if (selectedValue.includes(value)) {
@@ -80,43 +68,31 @@ export const SelectMulti: FC<ISelectMultiProps> = ({
8068
value={selectedValue.join(',')}
8169
readOnly
8270
/>
83-
<TriggerContainer>
84-
<Trigger
85-
disabled={disabled}
86-
expanded={isOpen}
87-
id={id ? `${id}-trigger` : undefined}
88-
label={emptyValue || 'Select value'}
89-
onClick={toggle}
90-
triggerRef={triggerRef}
91-
theme={theme}
92-
/>
93-
{isOpen ? (
94-
<Popover theme={theme}>
95-
{values.map(({ value, label }) => (
96-
<Option
97-
key={value}
98-
value={value}
99-
label={label}
100-
selected={selectedValue.includes(value)}
101-
onClick={handleToggleValue}
102-
theme={theme}
103-
/>
104-
))}
105-
</Popover>
106-
) : null}
107-
</TriggerContainer>
108-
<Tags>
109-
{selectedOptions.map(({ value, label }) => (
110-
<Tag
111-
key={value}
112-
disabled={disabled}
113-
label={label}
114-
value={value}
115-
onRemove={onDelete}
116-
theme={theme}
117-
/>
118-
))}
119-
</Tags>
71+
<Trigger
72+
disabled={disabled}
73+
expanded={isOpen}
74+
id={id ? `${id}-trigger` : undefined}
75+
label={summary.text || emptyValue || 'Select value'}
76+
badgeContent={summary.hiddenCount > 0 ? `+${summary.hiddenCount}` : undefined}
77+
onClick={toggle}
78+
title={title}
79+
triggerRef={triggerRef}
80+
theme={theme}
81+
/>
82+
{isOpen ? (
83+
<Popover theme={theme}>
84+
{values.map(({ value, label }) => (
85+
<Option
86+
key={value}
87+
value={value}
88+
label={label}
89+
selected={selectedValue.includes(value)}
90+
onClick={handleToggleValue}
91+
theme={theme}
92+
/>
93+
))}
94+
</Popover>
95+
) : null}
12096
</Container>
12197
);
12298
};

src/form/select.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import { Popover } from './popover';
99
const Container = styled.div`
1010
position: relative;
1111
display: inline-block;
12+
width: var(--query-builder-control-width, 160px);
13+
min-width: var(--query-builder-control-min-width, 160px);
1214
max-width: 100%;
1315
`;
1416

0 commit comments

Comments
 (0)