@xh/hoist 73.0.0-SNAPSHOT.1746829743606 → 73.0.0-SNAPSHOT.1746830066260

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/CHANGELOG.md CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  ## v73.0.0-SNAPSHOT - unreleased
4
4
 
5
+ ### 🎁 New Features
6
+
7
+ * Added `PopoverFilterChooser`, which wraps `FilterChooser` in a `Popover` to allow it to expand
8
+ vertically when used in a `Toolbar`.
9
+
5
10
  ### 💥 Breaking Changes (upgrade difficulty: 🟢 TRIVIAL - minor upgrade to Hoist Core)
6
11
 
7
12
  * Requires `hoist-core >= 30.1.0` with new APIs to support the consolidated Admin Console "Clients"
@@ -11,7 +11,7 @@ import {grid} from '@xh/hoist/cmp/grid';
11
11
  import {div, filler, hframe} from '@xh/hoist/cmp/layout';
12
12
  import {creates, hoistCmp} from '@xh/hoist/core';
13
13
  import {button, buttonGroup, colChooserButton, exportButton} from '@xh/hoist/desktop/cmp/button';
14
- import {filterChooser} from '@xh/hoist/desktop/cmp/filter';
14
+ import {popoverFilterChooser} from '@xh/hoist/desktop/cmp/filter';
15
15
  import {formField} from '@xh/hoist/desktop/cmp/form';
16
16
  import {groupingChooser} from '@xh/hoist/desktop/cmp/grouping';
17
17
  import {dateInput, DateInputProps, select} from '@xh/hoist/desktop/cmp/input';
@@ -137,7 +137,7 @@ const filterChooserToggleButton = hoistCmp.factory<ActivityTrackingModel>(({mode
137
137
  const filterBar = hoistCmp.factory<ActivityTrackingModel>(({model}) => {
138
138
  return model.showFilterChooser
139
139
  ? toolbar(
140
- filterChooser({
140
+ popoverFilterChooser({
141
141
  flex: 1,
142
142
  enableClear: true
143
143
  })
@@ -10,7 +10,7 @@ import {fragment, hframe, vframe} from '@xh/hoist/cmp/layout';
10
10
  import {creates, hoistCmp} from '@xh/hoist/core';
11
11
  import {button, colChooserButton} from '@xh/hoist/desktop/cmp/button';
12
12
  import {errorMessage} from '@xh/hoist/cmp/error';
13
- import {filterChooser} from '@xh/hoist/desktop/cmp/filter';
13
+ import {popoverFilterChooser} from '@xh/hoist/desktop/cmp/filter';
14
14
  import {switchInput} from '@xh/hoist/desktop/cmp/input';
15
15
  import {panel} from '@xh/hoist/desktop/cmp/panel';
16
16
  import {recordActionBar} from '@xh/hoist/desktop/cmp/record';
@@ -44,7 +44,7 @@ export const rolePanel = hoistCmp.factory({
44
44
  selModel: gridModel.selModel
45
45
  }),
46
46
  '-',
47
- filterChooser({flex: 1}),
47
+ popoverFilterChooser({flex: 1}),
48
48
  '-',
49
49
  switchInput({
50
50
  bind: 'showInGroups',
@@ -79,6 +79,7 @@ export declare class FilterChooserModel extends HoistModel {
79
79
  favoritesIsOpen: boolean;
80
80
  unsupportedFilter: boolean;
81
81
  inputRef: import("react").RefObject<HTMLElement> & import("react").RefCallback<HTMLElement>;
82
+ get tagCount(): number;
82
83
  constructor({ fieldSpecs, fieldSpecDefaults, bind, valueSource, initialValue, initialFavorites, suggestFieldsWhenEmpty, sortFieldSuggestions, maxTags, maxResults, persistWith, introHelpText }?: FilterChooserConfig);
83
84
  /**
84
85
  * Set the value displayed by this control.
@@ -10,6 +10,8 @@ export interface FilterChooserProps extends HoistProps<FilterChooserModel>, Layo
10
10
  disabled?: boolean;
11
11
  /** True to show a "clear" button at the right of the control. Defaults to true. */
12
12
  enableClear?: boolean;
13
+ /** True to show count of filter tags next to the left icon. */
14
+ displayCount?: boolean;
13
15
  /** Icon to display inline on the left side of the input. */
14
16
  leftIcon?: ReactElement;
15
17
  /** Max-height of dropdown. Either a number in pixels or a valid CSS string, such as '80vh'. */
@@ -0,0 +1,10 @@
1
+ /// <reference types="react" />
2
+ import '@xh/hoist/desktop/register';
3
+ import './PopoverFilterChooser.scss';
4
+ import { FilterChooserProps } from './FilterChooser';
5
+ /**
6
+ * A wrapper around a FilterChooser that renders in a popover when opened, allowing it to expand
7
+ * vertically beyond the height of a toolbar.
8
+ * @see FilterChooser
9
+ */
10
+ export declare const PopoverFilterChooser: import("react").FC<FilterChooserProps>, popoverFilterChooser: import("@xh/hoist/core").ElementFactory<FilterChooserProps>;
@@ -1,2 +1,3 @@
1
1
  export * from './FilterChooser';
2
+ export * from './PopoverFilterChooser';
2
3
  export * from '@xh/hoist/cmp/filter';
@@ -145,6 +145,10 @@ export class FilterChooserModel extends HoistModel {
145
145
  @observable unsupportedFilter = false;
146
146
  inputRef = createObservableRef<HTMLElement>();
147
147
 
148
+ get tagCount(): number {
149
+ return this.selectValue?.length ?? 0;
150
+ }
151
+
148
152
  constructor({
149
153
  fieldSpecs,
150
154
  fieldSpecDefaults,
@@ -10,6 +10,11 @@
10
10
  display: flex;
11
11
  flex: 1;
12
12
  }
13
+
14
+ &__count {
15
+ margin: 7px 4px;
16
+ align-self: start;
17
+ }
13
18
  }
14
19
 
15
20
  .xh-filter-chooser-option {
@@ -16,6 +16,7 @@ import {withDefault} from '@xh/hoist/utils/js';
16
16
  import {splitLayoutProps} from '@xh/hoist/utils/react';
17
17
  import classNames from 'classnames';
18
18
  import {isEmpty, sortBy} from 'lodash';
19
+ import {badge} from '@xh/hoist/cmp/badge';
19
20
  import {ReactElement} from 'react';
20
21
  import './FilterChooser.scss';
21
22
 
@@ -26,6 +27,8 @@ export interface FilterChooserProps extends HoistProps<FilterChooserModel>, Layo
26
27
  disabled?: boolean;
27
28
  /** True to show a "clear" button at the right of the control. Defaults to true. */
28
29
  enableClear?: boolean;
30
+ /** True to show count of filter tags next to the left icon. */
31
+ displayCount?: boolean;
29
32
  /** Icon to display inline on the left side of the input. */
30
33
  leftIcon?: ReactElement;
31
34
  /** Max-height of dropdown. Either a number in pixels or a valid CSS string, such as '80vh'. */
@@ -47,10 +50,23 @@ export const [FilterChooser, filterChooser] = hoistCmp.withFactory<FilterChooser
47
50
  className: 'xh-filter-chooser',
48
51
  render({model, className, ...props}, ref) {
49
52
  const [layoutProps, chooserProps] = splitLayoutProps(props),
50
- {inputRef, suggestFieldsWhenEmpty, selectOptions, unsupportedFilter, favoritesIsOpen} =
51
- model,
52
- {autoFocus, enableClear, leftIcon, maxMenuHeight, menuPlacement, menuWidth} =
53
- chooserProps,
53
+ {
54
+ inputRef,
55
+ suggestFieldsWhenEmpty,
56
+ selectOptions,
57
+ unsupportedFilter,
58
+ favoritesIsOpen,
59
+ tagCount
60
+ } = model,
61
+ {
62
+ autoFocus,
63
+ enableClear,
64
+ displayCount,
65
+ leftIcon,
66
+ maxMenuHeight,
67
+ menuPlacement,
68
+ menuWidth
69
+ } = chooserProps,
54
70
  disabled = unsupportedFilter || chooserProps.disabled,
55
71
  placeholder = unsupportedFilter
56
72
  ? 'Unsupported filter (click to clear)'
@@ -61,42 +77,49 @@ export const [FilterChooser, filterChooser] = hoistCmp.withFactory<FilterChooser
61
77
  className,
62
78
  ...layoutProps,
63
79
  item: popover({
64
- item: select({
65
- flex: 1,
66
- height: layoutProps?.height,
67
- bind: 'selectValue',
68
- ref: inputRef,
80
+ item: hframe(
81
+ badge({
82
+ omit: !displayCount || tagCount < 1,
83
+ className: 'xh-filter-chooser__count',
84
+ item: tagCount
85
+ }),
86
+ select({
87
+ flex: 1,
88
+ height: layoutProps?.height,
89
+ bind: 'selectValue',
90
+ ref: inputRef,
69
91
 
70
- autoFocus,
71
- disabled,
72
- menuPlacement,
73
- menuWidth,
74
- placeholder,
75
- leftIcon: withDefault(leftIcon, Icon.filter()),
76
- enableClear: withDefault(enableClear, true),
92
+ autoFocus,
93
+ disabled,
94
+ menuPlacement,
95
+ menuWidth,
96
+ placeholder,
97
+ leftIcon: withDefault(leftIcon, Icon.filter()),
98
+ enableClear: withDefault(enableClear, true),
77
99
 
78
- enableMulti: true,
79
- queryFn: q => model.queryAsync(q),
80
- options: selectOptions,
81
- optionRenderer,
82
- rsOptions: {
83
- defaultOptions: suggestFieldsWhenEmpty,
84
- openMenuOnClick: suggestFieldsWhenEmpty,
85
- openMenuOnFocus: false,
86
- isOptionDisabled: opt => opt.type === 'msg',
87
- noOptionsMessage: () => null,
88
- loadingMessage: () => null,
89
- styles: {
90
- menuList: base => ({
91
- ...base,
92
- maxHeight: withDefault(maxMenuHeight, '50vh')
93
- })
94
- },
95
- components: {
96
- DropdownIndicator: () => favoritesIcon(model)
100
+ enableMulti: true,
101
+ queryFn: q => model.queryAsync(q),
102
+ options: selectOptions,
103
+ optionRenderer,
104
+ rsOptions: {
105
+ defaultOptions: suggestFieldsWhenEmpty,
106
+ openMenuOnClick: suggestFieldsWhenEmpty,
107
+ openMenuOnFocus: false,
108
+ isOptionDisabled: opt => opt.type === 'msg',
109
+ noOptionsMessage: () => null,
110
+ loadingMessage: () => null,
111
+ styles: {
112
+ menuList: base => ({
113
+ ...base,
114
+ maxHeight: withDefault(maxMenuHeight, '50vh')
115
+ })
116
+ },
117
+ components: {
118
+ DropdownIndicator: () => favoritesIcon(model)
119
+ }
97
120
  }
98
- }
99
- }),
121
+ })
122
+ ),
100
123
  content: favoritesMenu(),
101
124
  isOpen: favoritesIsOpen,
102
125
  position: 'bottom-right',
@@ -0,0 +1,42 @@
1
+ /*
2
+ * This file belongs to Hoist, an application development toolkit
3
+ * developed by Extremely Heavy Industries (www.xh.io | info@xh.io)
4
+ *
5
+ * Copyright © 2025 Extremely Heavy Industries Inc.
6
+ */
7
+ .xh-popover-filter-chooser {
8
+ & > .bp5-popover-target {
9
+ display: flex;
10
+ flex: 1;
11
+ }
12
+
13
+ // Extra class names required to override the default styles of the popover
14
+ &__popover.bp5-popover.bp5-minimal {
15
+ margin-top: -15px !important;
16
+ box-shadow: none;
17
+
18
+ .bp5-popover-content {
19
+ background: transparent;
20
+ }
21
+
22
+ .xh-select__value-container--is-multi {
23
+ height: unset;
24
+ line-height: unset;
25
+ }
26
+ }
27
+
28
+ &__filter-chooser {
29
+ .xh-select__value-container--is-multi {
30
+ overflow-y: hidden !important;
31
+ }
32
+
33
+ .xh-select {
34
+ .xh-select__control--is-disabled {
35
+ background: var(--xh-input-bg);
36
+ }
37
+ .xh-select__multi-value--is-disabled .xh-select__multi-value__label {
38
+ color: unset;
39
+ }
40
+ }
41
+ }
42
+ }
@@ -0,0 +1,104 @@
1
+ /*
2
+ * This file belongs to Hoist, an application development toolkit
3
+ * developed by Extremely Heavy Industries (www.xh.io | info@xh.io)
4
+ *
5
+ * Copyright © 2025 Extremely Heavy Industries Inc.
6
+ */
7
+
8
+ import {hoistCmp, HoistModel, lookup, useLocalModel, uses} from '@xh/hoist/core';
9
+ import {bindable, makeObservable} from '@xh/hoist/mobx';
10
+ import {box, hframe} from '@xh/hoist/cmp/layout';
11
+ import '@xh/hoist/desktop/register';
12
+ import {popover} from '@xh/hoist/kit/blueprint';
13
+ import {getLayoutProps} from '@xh/hoist/utils/react';
14
+ import './PopoverFilterChooser.scss';
15
+ import {filterChooser, FilterChooserProps} from './FilterChooser';
16
+ import {FilterChooserModel} from '@xh/hoist/cmp/filter';
17
+
18
+ /**
19
+ * A wrapper around a FilterChooser that renders in a popover when opened, allowing it to expand
20
+ * vertically beyond the height of a toolbar.
21
+ * @see FilterChooser
22
+ */
23
+ export const [PopoverFilterChooser, popoverFilterChooser] =
24
+ hoistCmp.withFactory<FilterChooserProps>({
25
+ model: uses(FilterChooserModel),
26
+ className: 'xh-popover-filter-chooser',
27
+ render({model, className, ...props}, ref) {
28
+ const layoutProps = getLayoutProps(props),
29
+ impl = useLocalModel(PopoverFilterChooserLocalModel);
30
+
31
+ return box({
32
+ ref,
33
+ className,
34
+ ...layoutProps,
35
+ item: popover({
36
+ isOpen: impl.popoverIsOpen,
37
+ popoverClassName: 'xh-popover-filter-chooser__popover',
38
+ item: hframe(
39
+ filterChooser({
40
+ model,
41
+ // Omit when popover is open to force update the inputRef
42
+ omit: impl.popoverIsOpen,
43
+ className: 'xh-popover-filter-chooser__filter-chooser',
44
+ displayCount: true,
45
+ ...props,
46
+ disabled: true
47
+ })
48
+ ),
49
+ content: filterChooser({
50
+ model,
51
+ displayCount: true,
52
+ ...props
53
+ }),
54
+ matchTargetWidth: true,
55
+ minimal: true,
56
+ position: 'bottom',
57
+ onInteraction: open => {
58
+ if (open) {
59
+ impl.open();
60
+ } else {
61
+ impl.close();
62
+ }
63
+ }
64
+ })
65
+ });
66
+ }
67
+ });
68
+
69
+ class PopoverFilterChooserLocalModel extends HoistModel {
70
+ override xhImpl = true;
71
+
72
+ @lookup(FilterChooserModel)
73
+ model: FilterChooserModel;
74
+
75
+ @bindable
76
+ popoverIsOpen: boolean = false;
77
+
78
+ get displaySelectValue() {
79
+ return this.model.selectValue[0];
80
+ }
81
+
82
+ constructor() {
83
+ super();
84
+ makeObservable(this);
85
+ }
86
+
87
+ open() {
88
+ this.popoverIsOpen = true;
89
+
90
+ // Focus and open the menu when rendered
91
+ this.addReaction({
92
+ when: () => !!this.model.inputRef.current,
93
+ run: () => {
94
+ const inputRef = this.model.inputRef.current;
95
+ inputRef.focus();
96
+ (inputRef as any).reactSelectRef.current.select.onMenuOpen();
97
+ }
98
+ });
99
+ }
100
+
101
+ close() {
102
+ this.popoverIsOpen = false;
103
+ }
104
+ }
@@ -1,2 +1,3 @@
1
1
  export * from './FilterChooser';
2
+ export * from './PopoverFilterChooser';
2
3
  export * from '@xh/hoist/cmp/filter';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xh/hoist",
3
- "version": "73.0.0-SNAPSHOT.1746829743606",
3
+ "version": "73.0.0-SNAPSHOT.1746830066260",
4
4
  "description": "Hoist add-on for building and deploying React Applications.",
5
5
  "repository": "github:xh/hoist-react",
6
6
  "homepage": "https://xh.io",