@xh/hoist 66.0.0 β†’ 66.0.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/CHANGELOG.md CHANGED
@@ -1,19 +1,38 @@
1
1
  # Changelog
2
2
 
3
+ ## 66.0.2 - 2024-07-17
4
+
5
+ ### 🐞 Bug Fixes
6
+
7
+ * Improved redirect handling within beta `MsalClient` to use Hoist-provided blank URL (an empty,
8
+ static page) for all iFrame-based "silent" token requests, as per MS recommendations. Intended to
9
+ avoid potential race conditions triggered by redirecting to the base app URL in these cases.
10
+ * Fixed bug where `ContextMenu` items could be improperly positioned.
11
+ * ⚠️Note that `MenuItems` inside a desktop `ContextMenu` are now rendered in a portal, outside
12
+ the normal component hierarchy, to ensures that menu items are positioned properly relative to
13
+ their parent. It should not affect most apps, but could impact menu style customizations that
14
+ rely on specific CSS selectors targeting the previous DOM structure.
15
+
16
+ ## 66.0.1 - 2024-07-10
17
+
18
+ ### 🐞 Bug Fixes
19
+
20
+ * Fixed bug where inline grid edit of `NumberInput` was lost after quick navigation.
21
+
3
22
  ## 66.0.0 - 2024-07-09
4
23
 
5
- ### πŸ’₯ Breaking Changes (upgrade difficulty: 🟠 MEDIUM - minor adjustments to client-side auth)
24
+ ### πŸ’₯ Breaking Changes (upgrade difficulty: 🟒 LOW - minor adjustments to client-side auth)
6
25
 
7
26
  * New `HoistAuthModel` exposes the client-side authentication lifecycle via a newly consolidated,
8
27
  overridable API. This new API provides more easy customization of auth across all client-side
9
28
  apps by being easily overrideable and specified via the `AppSpec` passed to `XH.renderApp()`.
10
- * In most cases, upgrading should be a simple matter of moving code
11
- from `HoistAppModel.preAuthInitAsync()` and `logoutAsync()` (both removed) to new overrides
12
- of `HoistAuthModel.completeAuthAsync()` and `logoutAsync()`.
29
+ * In most cases, upgrading should be a simple matter of moving code from `HoistAppModel` methods
30
+ `preAuthInitAsync()` and `logoutAsync()` (removed by this change) to new `HoistAuthModel`
31
+ methods `completeAuthAsync()` and `logoutAsync()`.
13
32
 
14
33
  ### 🎁 New Features
15
34
 
16
- * New option for `XH.reloadApp` to reload specific app path.
35
+ * Added option to `XH.reloadApp()` to reload specific app path.
17
36
  * Added `headerTooltip` prop to `ColumnGroup`.
18
37
 
19
38
  ### 🐞 Bug Fixes
@@ -23,7 +42,7 @@
23
42
  an unexpected gap across the bottom of the screen. Includes fallback for secure client browsers
24
43
  that don't support dynamic viewport units.
25
44
  * Updated mobile `TabContainer` to flex properly within flexbox containers.
26
- * Fixed timing issue with missing validation for records added immediately to new store.
45
+ * Fixed timing issue with missing validation for records added immediately to a new `Store`.
27
46
  * Fixed CSS bug in which date picker dates wrapped when `dateEditor` used in a grid in a dialog.
28
47
 
29
48
  ## 65.0.0 - 2024-06-26
@@ -269,13 +288,24 @@ details.
269
288
  There are some common breaking changes that most/many apps will need to address:
270
289
 
271
290
  * CSS rules with the `bp4-` prefix should be updated to use the `bp5-` prefix.
272
- * For `popover` and `tooltip` components, replace `target` with `item` if using elementFactory.
273
- If using JSX, replace `target` prop with a child element. Also applies to the mobile `popover`.
274
- * Popovers no longer have a popover-wrapper element - remove/replace any CSS rules
275
- targeting `bp4-popover-wrapper`.
276
- * All components which render popovers now depend
277
- on [`popper.js v2.x`](https://popper.js.org/docs/v2/). Complex customizations to popovers may
278
- need to be reworked.
291
+ * Popovers
292
+ * For `popover` and `tooltip` components, replace `target` with `item` if using elementFactory.
293
+ If using JSX, replace `target` prop with a child element. Also applies to the
294
+ mobile `popover`.
295
+ * Popovers no longer have a popover-wrapper element - remove/replace any CSS rules
296
+ targeting `bp4-popover-wrapper`.
297
+ * All components which render popovers now depend
298
+ on [`popper.js v2.x`](https://popper.js.org/docs/v2/). Complex customizations to popovers may
299
+ need to be reworked.
300
+ * A breaking change to `Popover` in BP5 was splitting the `boundary` prop into `rootBoundary`
301
+ and `boundary`:
302
+ Popovers were frequently set up with `boundary: 'viewport'`, which is no longer valid since
303
+ "viewport" can be assigned to the `rootBoundary` but not to the `boundary`.
304
+ However, viewport is the DEFAULT value for `rootBoundary`
305
+ per [popper.js docs](https://popper.js.org/docs/v2/utils/detect-overflow/#boundary),
306
+ so `boundary: 'viewport'` should be safe to remove entirely.
307
+ * [see Blueprint's Popover2 migration guide](https://github.com/palantir/blueprint/wiki/Popover2-migration)
308
+ * [see Popover2's `boundary` & `rootBoundary` docs](https://popper.js.org/docs/v2/utils/detect-overflow/#boundary)
279
309
  * Where applicable, the former `elementRef` prop has been replaced by the simpler, more
280
310
  straightforward `ref` prop using `React.forwardRef()` - e.g. Hoist's `button.elementRef` prop
281
311
  becomes just `ref`. Review your app for uses of `elementRef`.
@@ -17,7 +17,7 @@ import {wait} from '@xh/hoist/promise';
17
17
  import {compact, groupBy, mapValues} from 'lodash';
18
18
  import moment from 'moment/moment';
19
19
  import {RoleEditorModel} from './editor/RoleEditorModel';
20
- import {HoistRole, RoleMemberType, RoleModuleConfig} from './Types';
20
+ import {HoistRole, RoleModuleConfig} from './Types';
21
21
 
22
22
  export class RoleModel extends HoistModel {
23
23
  static PERSIST_WITH = {localStorageKey: 'xhAdminRolesState'};
@@ -100,7 +100,26 @@ export class RoleModel extends HoistModel {
100
100
  this.gridModel.clear();
101
101
  }
102
102
 
103
+ async createAsync(roleSpec?: HoistRole): Promise<void> {
104
+ if (this.readonly) return;
105
+
106
+ const addedRole = await this.roleEditorModel.createAsync(roleSpec);
107
+ if (!addedRole) return;
108
+ await this.refreshAsync();
109
+ await this.gridModel.selectAsync(addedRole.name);
110
+ }
111
+
112
+ async editAsync(role: HoistRole): Promise<void> {
113
+ if (this.readonly) return;
114
+
115
+ const updatedRole = await this.roleEditorModel.editAsync(role);
116
+ if (!updatedRole) return;
117
+ await this.refreshAsync();
118
+ }
119
+
103
120
  async deleteAsync(role: HoistRole): Promise<boolean> {
121
+ if (this.readonly) return false;
122
+
104
123
  const confirm = await XH.confirm({
105
124
  icon: Icon.warning(),
106
125
  title: 'Confirm delete?',
@@ -108,17 +127,18 @@ export class RoleModel extends HoistModel {
108
127
  confirmProps: {intent: 'danger', text: 'Confirm Delete'}
109
128
  });
110
129
  if (!confirm) return false;
130
+
111
131
  await XH.fetchJson({
112
132
  url: `roleAdmin/delete/${role.name}`,
113
133
  method: 'DELETE'
114
134
  });
115
- this.refreshAsync();
135
+ await this.refreshAsync();
116
136
  return true;
117
137
  }
118
138
 
119
- // -------------------------------
139
+ //------------------
120
140
  // Actions
121
- // -------------------------------
141
+ //------------------
122
142
  addAction(): RecordActionSpec {
123
143
  return {
124
144
  text: 'Add',
@@ -142,7 +162,7 @@ export class RoleModel extends HoistModel {
142
162
  };
143
163
  }
144
164
 
145
- cloneAction(): RecordActionSpec {
165
+ private cloneAction(): RecordActionSpec {
146
166
  return {
147
167
  text: 'Clone',
148
168
  icon: Icon.copy(),
@@ -154,7 +174,7 @@ export class RoleModel extends HoistModel {
154
174
  };
155
175
  }
156
176
 
157
- deleteAction(): RecordActionSpec {
177
+ private deleteAction(): RecordActionSpec {
158
178
  return {
159
179
  text: 'Delete',
160
180
  icon: Icon.delete(),
@@ -181,16 +201,10 @@ export class RoleModel extends HoistModel {
181
201
  }
182
202
  };
183
203
  }
184
- async editAsync(role: HoistRole): Promise<void> {
185
- const updatedRole = await this.roleEditorModel.editAsync(role);
186
- if (!updatedRole) return;
187
- await this.refreshAsync();
188
- }
189
204
 
190
- // -------------------------------
205
+ //------------------
191
206
  // Implementation
192
- // -------------------------------
193
-
207
+ //------------------
194
208
  private displayRoles() {
195
209
  const {gridModel} = this,
196
210
  gridData = this.showInGroups
@@ -250,13 +264,6 @@ export class RoleModel extends HoistModel {
250
264
  return root;
251
265
  }
252
266
 
253
- private async createAsync(roleSpec?: HoistRole): Promise<void> {
254
- const addedRole = await this.roleEditorModel.createAsync(roleSpec);
255
- if (!addedRole) return;
256
- await this.refreshAsync();
257
- await this.gridModel.selectAsync(addedRole.name);
258
- }
259
-
260
267
  private createGridModel(): GridModel {
261
268
  return new GridModel({
262
269
  treeMode: true,
@@ -272,13 +279,6 @@ export class RoleModel extends HoistModel {
272
279
  'xh-grid-clear-background-color': ({data}) => !data.data.isGroupRow
273
280
  },
274
281
  headerMenuDisplay: 'hover',
275
- onRowDoubleClicked: ({data: record}) => {
276
- if (!this.readonly && record && record.data.isGroupRow) {
277
- this.roleEditorModel
278
- .editAsync(record.data)
279
- .then(role => role && this.refreshAsync());
280
- }
281
- },
282
282
  persistWith: {...this.persistWith, path: 'mainGrid'},
283
283
  store: {
284
284
  idSpec: ({id, name}) => {
@@ -344,7 +344,12 @@ export class RoleModel extends HoistModel {
344
344
  '-',
345
345
  this.groupByAction(),
346
346
  ...GridModel.defaultContextMenu
347
- ]
347
+ ],
348
+ onRowDoubleClicked: ({data: record}) => {
349
+ if (record && !record.data.isGroupRow) {
350
+ this.editAsync(record.data as HoistRole);
351
+ }
352
+ }
348
353
  });
349
354
  }
350
355
 
@@ -384,15 +389,4 @@ export class RoleModel extends HoistModel {
384
389
  persistWith: {...RoleModel.PERSIST_WITH, path: 'mainFilterChooser'}
385
390
  });
386
391
  }
387
-
388
- private getFieldForMemberType(type: RoleMemberType, effective: boolean): string {
389
- switch (type) {
390
- case 'USER':
391
- return effective ? 'effectiveUserNames' : 'users';
392
- case 'DIRECTORY_GROUP':
393
- return effective ? 'effectiveDirectoryGroupNames' : 'directoryGroups';
394
- case 'ROLE':
395
- return effective ? 'effectiveRoleNames' : 'roles';
396
- }
397
- }
398
392
  }
@@ -24,19 +24,18 @@ export declare class RoleModel extends HoistModel {
24
24
  doLoadAsync(loadSpec: LoadSpec): Promise<void>;
25
25
  selectRoleAsync(name: string): Promise<void>;
26
26
  clear(): void;
27
+ createAsync(roleSpec?: HoistRole): Promise<void>;
28
+ editAsync(role: HoistRole): Promise<void>;
27
29
  deleteAsync(role: HoistRole): Promise<boolean>;
28
30
  addAction(): RecordActionSpec;
29
31
  editAction(): RecordActionSpec;
30
- cloneAction(): RecordActionSpec;
31
- deleteAction(): RecordActionSpec;
32
+ private cloneAction;
33
+ private deleteAction;
32
34
  private groupByAction;
33
- editAsync(role: HoistRole): Promise<void>;
34
35
  private displayRoles;
35
36
  private ensureInitializedAsync;
36
37
  private processRolesFromServer;
37
38
  private processRolesForTreeGrid;
38
- private createAsync;
39
39
  private createGridModel;
40
40
  private createFilterChooserModel;
41
- private getFieldForMemberType;
42
41
  }
@@ -2,7 +2,7 @@ import { HoistProps, MenuItemLike } from '@xh/hoist/core';
2
2
  import '@xh/hoist/desktop/register';
3
3
  import { ReactElement } from 'react';
4
4
  /**
5
- * A context menu is specified as an array of items, a function to generate one from a click, or
5
+ * A context menu is specified as an array of items, a function to generate one from a click, or
6
6
  * a full element representing a contextMenu Component.
7
7
  */
8
8
  export type ContextMenuSpec = MenuItemLike[] | ((e: MouseEvent) => MenuItemLike[]) | ReactElement;
@@ -10,12 +10,11 @@ export interface ContextMenuProps extends HoistProps {
10
10
  menuItems: MenuItemLike[];
11
11
  }
12
12
  /**
13
- * ContextMenu
13
+ * Component for a right-click context menu. Not typically used directly by applications - use
14
+ * the {@link useContextMenu} hook to add a context menu to an app component, or leverage Panel's
15
+ * built-in support via {@link PanelProps.contextMenu}.
14
16
  *
15
- * Not typically used directly by applications. To add a Context Menu to an application
16
- * see ContextMenuHost, or the `contextMenu` prop on panel.
17
- *
18
- * See {@link GridContextMenu} to specify a context menu on Grid and DataView components.
17
+ * See {@link GridContextMenuSpec} to specify a context menu on `Grid` and `DataView` components.
19
18
  * That API will receive specific information about the current selection
20
19
  */
21
20
  export declare const ContextMenu: import("react").FC<ContextMenuProps>, contextMenu: import("@xh/hoist/core").ElementFactory<ContextMenuProps>;
@@ -16,7 +16,7 @@ export interface PanelProps extends HoistProps<PanelModel>, Omit<BoxProps, 'titl
16
16
  icon?: ReactElement;
17
17
  /** Icon to be used when the panel is collapsed. Defaults to `icon`. */
18
18
  collapsedIcon?: ReactElement;
19
- /** Context Menu to show on context clicking this panel. */
19
+ /** Context menu to show on a right-click within this panel. */
20
20
  contextMenu?: ContextMenuSpec;
21
21
  /**
22
22
  * Specification of hotkeys as prescribed by blueprint.
@@ -1,11 +1,11 @@
1
1
  import { ContextMenuSpec } from '@xh/hoist/desktop/cmp/contextmenu/ContextMenu';
2
2
  import { ReactElement } from 'react';
3
3
  /**
4
- * Hook to add context menu support to a component.
4
+ * Hook to add a right-click context menu to a component.
5
5
  *
6
- * @param child - element to be given context menu support. Must specify Component
7
- * that takes react context menu event as a prop (e.g. boxes, panel, div, etc).
8
- * @param spec - Context Menu to be shown. If null, or the number of items is empty,
9
- * no menu will be rendered, and the event will be consumed.
6
+ * @param child - element to be given context menu support. Must specify a component that takes
7
+ * the React `onContextMenu` event as a prop (e.g. boxes, panel, div, etc.)
8
+ * @param spec - spec the menu to be shown. If null, or the number of items is empty, no menu will
9
+ * be rendered and the event will be consumed.
10
10
  */
11
11
  export declare function useContextMenu(child?: ReactElement, spec?: ContextMenuSpec): ReactElement;
@@ -5,13 +5,13 @@ export interface BaseOAuthClientConfig<S> {
5
5
  /** Client ID (GUID) of your app registered with your Oauth provider. */
6
6
  clientId: string;
7
7
  /**
8
- * The redirect URL where authentication responses can be received by your application.
8
+ * Redirect URL where authentication responses can be received by your application.
9
9
  * It must exactly match one of the redirect URIs registered in the relevant OAuth authority.
10
10
  * Default is 'APP_BASE_URL' which will be replaced with the current app's base URL.
11
11
  */
12
12
  redirectUrl?: 'APP_BASE_URL' | string;
13
13
  /**
14
- * The redirect URL after a successful logout.
14
+ * Redirect URL after a successful logout.
15
15
  * Default is 'APP_BASE_URL' which will be replaced with the current app's base URL.
16
16
  */
17
17
  postLogoutRedirectUrl?: 'APP_BASE_URL' | string;
@@ -72,7 +72,7 @@ export interface BaseOAuthClientConfig<S> {
72
72
  export declare abstract class BaseOAuthClient<C extends BaseOAuthClientConfig<S>, S> extends HoistBase {
73
73
  /** Config loaded from UI server + init method. */
74
74
  protected config: C;
75
- /** Id Scopes */
75
+ /** ID Scopes */
76
76
  protected idScopes: string[];
77
77
  /** Specification for Access Tokens **/
78
78
  protected accessSpecs: Record<string, S>;
@@ -97,7 +97,7 @@ export declare abstract class BaseOAuthClient<C extends BaseOAuthClientConfig<S>
97
97
  */
98
98
  getIdTokenAsync(): Promise<Token>;
99
99
  /**
100
- * Get a Access token.
100
+ * Get an Access token.
101
101
  */
102
102
  getAccessTokenAsync(key: string): Promise<Token>;
103
103
  /**
@@ -107,7 +107,7 @@ export declare abstract class BaseOAuthClient<C extends BaseOAuthClientConfig<S>
107
107
  /**
108
108
  * The last authenticated OAuth username.
109
109
  *
110
- * Provided to facilitate more efficient re-login via SSO or otherwise. Cleared on logout.
110
+ * Provided to facilitate more efficient re-login via SSO or otherwise. Cleared on logout.
111
111
  * Note: not necessarily a currently authenticated user, and not necessarily the Hoist username.
112
112
  */
113
113
  getSelectedUsername(): string;
@@ -2,14 +2,13 @@ import { LogLevel } from '@azure/msal-browser';
2
2
  import { Token, TokenMap } from '@xh/hoist/security/Token';
3
3
  import { BaseOAuthClient, BaseOAuthClientConfig } from '../BaseOAuthClient';
4
4
  export interface MsalClientConfig extends BaseOAuthClientConfig<MsalTokenSpec> {
5
- /** Tenant ID (GUID) of your organization */
6
- tenantId: string;
7
5
  /**
8
- * You can use a specific authority, like "https://login.microsoftonline.com/[tenantId]".
9
- * Enterprise apps will most likely use a specific authority.
10
- * MSAL Browser Lib defaults authority to "https://login.microsoftonline.com/common"
6
+ * Authority for your organization's tenant: `https://login.microsoftonline.com/[tenantId]`.
7
+ * MSAL defaults to their "common" tenant (https://login.microsoftonline.com/common") to support
8
+ * auth with personal MS accounts, but enterprise/Hoist apps will almost certainly use a
9
+ * specific authority to point to their own private/corporate tenant.
11
10
  */
12
- authority?: string;
11
+ authority: string;
13
12
  /**
14
13
  * A hint about the tenant or domain that the user should use to sign in.
15
14
  * The value of the domain hint is a registered domain for the tenant.
@@ -63,10 +62,13 @@ export interface MsalTokenSpec {
63
62
  * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/token-lifetimes.md
64
63
  * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/login-user.md
65
64
  *
66
- * TODO: The handling of `ssoSilent` and `initRefreshTokenExpirationOffsetSecs` in this library
67
- * require 3rd party cookies to be enabled in the browser so that MSAL can load contact in a
68
- * hidden iFrame If its *not* enabled, we may be doing extra work. Consider checking 3rd party
69
- * cookie support and adding conditional behavior?
65
+ * Also see this doc re. use of blankUrl as redirectUri for all "silent" token requests:
66
+ * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/errors.md#issues-caused-by-the-redirecturi-page
67
+ *
68
+ * TODO: The handling of `ssoSilent` and `initRefreshTokenExpirationOffsetSecs` in this library
69
+ * require 3rd party cookies to be enabled in the browser so that MSAL can load contact in a
70
+ * hidden iFrame If its *not* enabled, we may be doing extra work. Consider checking 3rd party
71
+ * cookie support and adding conditional behavior?
70
72
  */
71
73
  export declare class MsalClient extends BaseOAuthClient<MsalClientConfig, MsalTokenSpec> {
72
74
  private client;
@@ -13,7 +13,7 @@ import {clone, isEmpty, isString} from 'lodash';
13
13
  import {isValidElement, ReactElement, ReactNode} from 'react';
14
14
 
15
15
  /**
16
- * A context menu is specified as an array of items, a function to generate one from a click, or
16
+ * A context menu is specified as an array of items, a function to generate one from a click, or
17
17
  * a full element representing a contextMenu Component.
18
18
  */
19
19
  export type ContextMenuSpec = MenuItemLike[] | ((e: MouseEvent) => MenuItemLike[]) | ReactElement;
@@ -23,12 +23,11 @@ export interface ContextMenuProps extends HoistProps {
23
23
  }
24
24
 
25
25
  /**
26
- * ContextMenu
26
+ * Component for a right-click context menu. Not typically used directly by applications - use
27
+ * the {@link useContextMenu} hook to add a context menu to an app component, or leverage Panel's
28
+ * built-in support via {@link PanelProps.contextMenu}.
27
29
  *
28
- * Not typically used directly by applications. To add a Context Menu to an application
29
- * see ContextMenuHost, or the `contextMenu` prop on panel.
30
- *
31
- * See {@link GridContextMenu} to specify a context menu on Grid and DataView components.
30
+ * See {@link GridContextMenuSpec} to specify a context menu on `Grid` and `DataView` components.
32
31
  * That API will receive specific information about the current selection
33
32
  */
34
33
  export const [ContextMenu, contextMenu] = hoistCmp.withFactory<ContextMenuProps>({
@@ -72,6 +71,7 @@ function parseItems(items: MenuItemLike[]): ReactNode[] {
72
71
  intent: item.intent,
73
72
  className: item.className,
74
73
  onClick: item.actionFn ? () => wait().then(item.actionFn) : null, // do async to allow menu to close
74
+ popoverProps: {usePortal: true},
75
75
  disabled: item.disabled,
76
76
  items
77
77
  });
@@ -4,6 +4,8 @@
4
4
  *
5
5
  * Copyright Β© 2024 Extremely Heavy Industries Inc.
6
6
  */
7
+ import {withDefault} from '@xh/hoist/utils/js';
8
+ import {isNil} from 'lodash';
7
9
  import {useCallback, useEffect} from 'react';
8
10
  import {CustomCellEditorProps, useGridCellEditor} from '@ag-grid-community/react';
9
11
  import {hoistCmp} from '@xh/hoist/core';
@@ -22,6 +24,16 @@ export const [NumberEditor, numberEditor] = hoistCmp.withFactory<NumberEditorPro
22
24
  observer: false,
23
25
  render(props, ref) {
24
26
  useNumberGuard(props.agParams);
27
+
28
+ // Make sure to override the NumberEditor debounce to 0 to prevent a bug where rapid changes are not saved.
29
+ if (isNil(props.inputProps)) props.inputProps = {};
30
+ // @ts-ignore
31
+ props.inputProps.commitOnChangeDebounce = withDefault(
32
+ // @ts-ignore
33
+ props.inputProps.commitOnChangeDebounce,
34
+ 0
35
+ );
36
+
25
37
  return useInlineEditorModel(numberInput, props, ref);
26
38
  }
27
39
  });
@@ -11,9 +11,9 @@ import '@xh/hoist/desktop/register';
11
11
  import {fmtNumber, parseNumber} from '@xh/hoist/format';
12
12
  import {numericInput} from '@xh/hoist/kit/blueprint';
13
13
  import {wait} from '@xh/hoist/promise';
14
- import {debounced, TEST_ID, throwIf, withDefault} from '@xh/hoist/utils/js';
14
+ import {TEST_ID, throwIf, withDefault} from '@xh/hoist/utils/js';
15
15
  import {getLayoutProps} from '@xh/hoist/utils/react';
16
- import {isNaN, isNil, isNumber, round} from 'lodash';
16
+ import {debounce, isNaN, isNil, isNumber, round} from 'lodash';
17
17
  import {KeyboardEventHandler, ReactElement, ReactNode, Ref, useLayoutEffect} from 'react';
18
18
 
19
19
  export interface NumberInputProps extends HoistProps, LayoutProps, StyleProps, HoistInputProps {
@@ -145,9 +145,12 @@ class NumberInputModel extends HoistInputModel {
145
145
  this.noteValueChange(valAsString);
146
146
  };
147
147
 
148
- @debounced(250)
148
+ /** TODO: Completely remove the debounce, or find and verify a reason why we need it set to 250. */
149
149
  override doCommitOnChangeInternal() {
150
- super.doCommitOnChangeInternal();
150
+ debounce(
151
+ () => super.doCommitOnChangeInternal(),
152
+ withDefault(this.componentProps.commitOnChangeDebounce, 250)
153
+ )();
151
154
  }
152
155
 
153
156
  override toInternal(val: number): number {
@@ -50,7 +50,7 @@ export interface PanelProps extends HoistProps<PanelModel>, Omit<BoxProps, 'titl
50
50
  /** Icon to be used when the panel is collapsed. Defaults to `icon`. */
51
51
  collapsedIcon?: ReactElement;
52
52
 
53
- /** Context Menu to show on context clicking this panel. */
53
+ /** Context menu to show on a right-click within this panel. */
54
54
  contextMenu?: ContextMenuSpec;
55
55
 
56
56
  /**
@@ -6,17 +6,17 @@
6
6
  */
7
7
  import {contextMenu, ContextMenuSpec} from '@xh/hoist/desktop/cmp/contextmenu/ContextMenu';
8
8
  import {showContextMenu} from '@xh/hoist/kit/blueprint';
9
- import {isArray, isFunction, isUndefined, isEmpty} from 'lodash';
10
- import {ReactElement} from 'react';
11
- import {cloneElement, isValidElement} from 'react';
9
+ import {logError} from '@xh/hoist/utils/js';
10
+ import {isArray, isEmpty, isFunction, isUndefined} from 'lodash';
11
+ import {cloneElement, isValidElement, ReactElement} from 'react';
12
12
 
13
13
  /**
14
- * Hook to add context menu support to a component.
14
+ * Hook to add a right-click context menu to a component.
15
15
  *
16
- * @param child - element to be given context menu support. Must specify Component
17
- * that takes react context menu event as a prop (e.g. boxes, panel, div, etc).
18
- * @param spec - Context Menu to be shown. If null, or the number of items is empty,
19
- * no menu will be rendered, and the event will be consumed.
16
+ * @param child - element to be given context menu support. Must specify a component that takes
17
+ * the React `onContextMenu` event as a prop (e.g. boxes, panel, div, etc.)
18
+ * @param spec - spec the menu to be shown. If null, or the number of items is empty, no menu will
19
+ * be rendered and the event will be consumed.
20
20
  */
21
21
  export function useContextMenu(child?: ReactElement, spec?: ContextMenuSpec): ReactElement {
22
22
  if (!child || isUndefined(spec)) return child;
@@ -24,11 +24,11 @@ export function useContextMenu(child?: ReactElement, spec?: ContextMenuSpec): Re
24
24
  const onContextMenu = (e: MouseEvent) => {
25
25
  let contextMenuOutput: any = spec;
26
26
 
27
- // 0) Skip if already consumed, otherwise consume (Adapted from Blueprint 'ContextMenuTarget')
27
+ // 0) Skip if already consumed, otherwise consume (adapted from BP `ContextMenuTarget`).
28
28
  if (e.defaultPrevented) return;
29
29
  e.preventDefault();
30
30
 
31
- // 1) Pre-process to an element (potentially via item list) or null
31
+ // 1) Pre-process to an element (potentially via item list) or null.
32
32
  if (isFunction(contextMenuOutput)) {
33
33
  contextMenuOutput = contextMenuOutput(e);
34
34
  }
@@ -38,11 +38,11 @@ export function useContextMenu(child?: ReactElement, spec?: ContextMenuSpec): Re
38
38
  : null;
39
39
  }
40
40
  if (contextMenuOutput && !isValidElement(contextMenuOutput)) {
41
- console.error("Incorrect specification of 'contextMenu' arg in useContextMenu()");
41
+ logError(`Incorrect specification of 'contextMenu' arg in useContextMenu()`);
42
42
  contextMenuOutput = null;
43
43
  }
44
44
 
45
- // 2) Render via blueprint!
45
+ // 2) Render via blueprint.
46
46
  if (contextMenuOutput) {
47
47
  showContextMenu(contextMenuOutput, {left: e.clientX, top: e.clientY});
48
48
  }
@@ -10,9 +10,9 @@ import {fmtNumber} from '@xh/hoist/format';
10
10
  import {input} from '@xh/hoist/kit/onsen';
11
11
  import '@xh/hoist/mobile/register';
12
12
  import {wait} from '@xh/hoist/promise';
13
- import {debounced, throwIf, withDefault} from '@xh/hoist/utils/js';
13
+ import {throwIf, withDefault} from '@xh/hoist/utils/js';
14
14
  import {getLayoutProps} from '@xh/hoist/utils/react';
15
- import {isNaN, isNil, isNumber, round} from 'lodash';
15
+ import {debounce, isNaN, isNil, isNumber, round} from 'lodash';
16
16
  import './NumberInput.scss';
17
17
 
18
18
  export interface NumberInputProps extends HoistProps, HoistInputProps, StyleProps, LayoutProps {
@@ -123,9 +123,12 @@ class NumberInputModel extends HoistInputModel {
123
123
  this.noteValueChange(ev.target.value);
124
124
  };
125
125
 
126
- @debounced(250)
126
+ /** TODO: Completely remove the debounce, or find and verify a reason why we need it set to 250. */
127
127
  override doCommitOnChangeInternal() {
128
- super.doCommitOnChangeInternal();
128
+ debounce(
129
+ () => super.doCommitOnChangeInternal(),
130
+ withDefault(this.componentProps.commitOnChangeDebounce, 250)
131
+ )();
129
132
  }
130
133
 
131
134
  override toInternal(val) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xh/hoist",
3
- "version": "66.0.0",
3
+ "version": "66.0.2",
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",