From ed1529cedc86d96e5a37e565819333313906c621 Mon Sep 17 00:00:00 2001 From: Colin Rudd Date: Tue, 28 May 2024 14:30:55 -0400 Subject: [PATCH 01/23] Restore support for ref prop in our components when written in JSX. --- core/HoistComponent.ts | 5 ++++- core/HoistProps.ts | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/core/HoistComponent.ts b/core/HoistComponent.ts index 2adad0b495..c339c8b356 100644 --- a/core/HoistComponent.ts +++ b/core/HoistComponent.ts @@ -47,7 +47,7 @@ import { /** * Type representing props passed to a HoistComponent's render function. * - * This type removes from its base type several props that are used by HoistComponent itself and + * This type removes from its base type several props that are pulled out by the HoistComponent itself and * not provided to the render function. */ export type RenderPropsOf

= P & { @@ -56,6 +56,9 @@ export type RenderPropsOf

= P & { /** Pre-processed by HoistComponent internals and attached to model. Never passed to render. */ modelRef: never; + + /** Pre-processed by HoistComponent internals. Passed as second argument. */ + ref: never; }; /** diff --git a/core/HoistProps.ts b/core/HoistProps.ts index 33b9d74be1..5a7333b82a 100644 --- a/core/HoistProps.ts +++ b/core/HoistProps.ts @@ -6,7 +6,7 @@ */ import {HoistModel} from '@xh/hoist/core'; import {Property} from 'csstype'; -import {CSSProperties, HTMLAttributes, ReactNode, Ref} from 'react'; +import {CSSProperties, HTMLAttributes, ReactNode, Ref, ForwardedRef} from 'react'; /** * Props interface for Hoist Components. @@ -41,6 +41,9 @@ export interface HoistProps { */ className?: string; + /** React Ref for this component. */ + ref?: ForwardedRef; + /** React children. */ children?: ReactNode; } From 96c1e63e0e1c07e9ea5885b11345296a79ed1ada Mon Sep 17 00:00:00 2001 From: Colin Rudd Date: Tue, 28 May 2024 14:33:30 -0400 Subject: [PATCH 02/23] CHANGELOG.md --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 232da80ef6..ee0942cad4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## 65.0.0-SNAPSHOT - unreleased +### 🐞 Bug Fixes +* Restore support for `ref` prop in Hoist components written in JSX. + ## 64.0.2 - 2024-05-23 ### ⚙️ Technical From 2010f89469d80e12714721cdbdb990f7a1d8e36e Mon Sep 17 00:00:00 2001 From: Colin Rudd Date: Tue, 28 May 2024 19:00:02 -0400 Subject: [PATCH 03/23] Reworked, much improved with Greg's help and review. Made hoist components limit ref prop only to those setup to handle it. --- cmp/dataview/DataView.ts | 3 +++ cmp/grid/Grid.ts | 3 +++ cmp/grid/helpers/GridCountLabel.ts | 20 ++++++++++++-------- cmp/input/HoistInputProps.ts | 3 +++ core/HoistComponent.ts | 13 +++---------- core/HoistProps.ts | 7 +++---- desktop/cmp/button/Button.ts | 4 +++- desktop/cmp/input/ButtonGroupInput.ts | 6 ++++-- desktop/cmp/pinpad/impl/PinPad.ts | 3 ++- mobile/appcontainer/ToastSource.ts | 3 ++- mobile/cmp/input/ButtonGroupInput.ts | 5 ++++- utils/react/LayoutPropUtils.ts | 4 ++-- 12 files changed, 44 insertions(+), 30 deletions(-) diff --git a/cmp/dataview/DataView.ts b/cmp/dataview/DataView.ts index 84c75eb795..4166020950 100644 --- a/cmp/dataview/DataView.ts +++ b/cmp/dataview/DataView.ts @@ -4,6 +4,7 @@ * * Copyright © 2024 Extremely Heavy Industries Inc. */ +import {ForwardedRef} from 'react'; import {AgGrid} from '@xh/hoist/cmp/ag-grid'; import {grid} from '@xh/hoist/cmp/grid'; import { @@ -34,6 +35,8 @@ export interface DataViewProps extends HoistProps, LayoutProps, T * Note that changes to these options after the component's initial render will be ignored. */ agOptions?: GridOptions; + + ref?: ForwardedRef; } /** diff --git a/cmp/grid/Grid.ts b/cmp/grid/Grid.ts index 94fec9b9ad..4d96e77810 100644 --- a/cmp/grid/Grid.ts +++ b/cmp/grid/Grid.ts @@ -4,6 +4,7 @@ * * Copyright © 2024 Extremely Heavy Industries Inc. */ +import {ForwardedRef} from 'react'; import composeRefs from '@seznam/compose-react-refs'; import {agGrid, AgGrid} from '@xh/hoist/cmp/ag-grid'; import {getTreeStyleClasses} from '@xh/hoist/cmp/grid'; @@ -68,6 +69,8 @@ export interface GridProps extends HoistProps, LayoutProps, TestSuppo * event after running its internal handler to associate the ag-Grid APIs with its model. */ onGridReady?: (e: GridReadyEvent) => void; + + ref?: ForwardedRef; } /** diff --git a/cmp/grid/helpers/GridCountLabel.ts b/cmp/grid/helpers/GridCountLabel.ts index ce4dae36ab..822fec2aaa 100644 --- a/cmp/grid/helpers/GridCountLabel.ts +++ b/cmp/grid/helpers/GridCountLabel.ts @@ -39,13 +39,16 @@ export const [GridCountLabel, gridCountLabel] = hoistCmp.withFactory; } diff --git a/core/HoistComponent.ts b/core/HoistComponent.ts index c339c8b356..9ac624de8a 100644 --- a/core/HoistComponent.ts +++ b/core/HoistComponent.ts @@ -48,18 +48,11 @@ import { * Type representing props passed to a HoistComponent's render function. * * This type removes from its base type several props that are pulled out by the HoistComponent itself and - * not provided to the render function. + * not provided to the render function. Ref is passed as a forwarded ref as the second argument + * to the render function. */ -export type RenderPropsOf

= P & { - /** Pre-processed by HoistComponent internals into a mounted model. Never passed to render. */ - modelConfig: never; - /** Pre-processed by HoistComponent internals and attached to model. Never passed to render. */ - modelRef: never; - - /** Pre-processed by HoistComponent internals. Passed as second argument. */ - ref: never; -}; +export type RenderPropsOf

= Omit; /** * Configuration for creating a Component. May be specified either as a render function, diff --git a/core/HoistProps.ts b/core/HoistProps.ts index 5a7333b82a..07f4aa267c 100644 --- a/core/HoistProps.ts +++ b/core/HoistProps.ts @@ -41,9 +41,6 @@ export interface HoistProps { */ className?: string; - /** React Ref for this component. */ - ref?: ForwardedRef; - /** React children. */ children?: ReactNode; } @@ -69,7 +66,9 @@ export interface DefaultHoistProps extends Ho export interface BoxProps extends LayoutProps, TestSupportProps, - Omit, 'onChange' | 'contextMenu'> {} + Omit, 'onChange' | 'contextMenu'> { + ref?: ForwardedRef; +} /** * Props for Components that accept standard HTML `style` attributes. diff --git a/desktop/cmp/button/Button.ts b/desktop/cmp/button/Button.ts index 80b9f14116..90b3998407 100644 --- a/desktop/cmp/button/Button.ts +++ b/desktop/cmp/button/Button.ts @@ -19,7 +19,7 @@ import {button as bpButton} from '@xh/hoist/kit/blueprint'; import {TEST_ID, withDefault} from '@xh/hoist/utils/js'; import {splitLayoutProps} from '@xh/hoist/utils/react'; import classNames from 'classnames'; -import {ReactElement, ReactNode} from 'react'; +import {ForwardedRef, ReactElement, ReactNode} from 'react'; import './Button.scss'; export interface ButtonProps @@ -44,6 +44,8 @@ export interface ButtonProps /** Alias for title. */ tooltip?: string; + + ref?: ForwardedRef; } /** diff --git a/desktop/cmp/input/ButtonGroupInput.ts b/desktop/cmp/input/ButtonGroupInput.ts index 9d6693e4eb..50ff95c700 100644 --- a/desktop/cmp/input/ButtonGroupInput.ts +++ b/desktop/cmp/input/ButtonGroupInput.ts @@ -8,6 +8,7 @@ import {HoistInputModel, HoistInputProps, useHoistInputModel} from '@xh/hoist/cm import {hoistCmp, HoistModel, Intent, XH} from '@xh/hoist/core'; import {Button, buttonGroup, ButtonGroupProps} from '@xh/hoist/desktop/cmp/button'; import '@xh/hoist/desktop/register'; +import {ButtonProps} from '@xh/hoist/desktop/cmp/button'; import {throwIf, warnIf, withDefault} from '@xh/hoist/utils/js'; import {getLayoutProps, getNonLayoutProps} from '@xh/hoist/utils/react'; import {castArray, filter, isEmpty, without} from 'lodash'; @@ -130,8 +131,9 @@ const cmp = hoistCmp.factory(({model, className, ...props throw XH.exception('ButtonGroupInput child must be a Button.'); } - const {value, intent: btnIntent} = button.props, - btnDisabled = disabled || button.props.disabled; + const props = button.props as ButtonProps, + {value, intent: btnIntent} = props, + btnDisabled = disabled || props.disabled; throwIf( (enableClear || enableMulti) && value == null, diff --git a/desktop/cmp/pinpad/impl/PinPad.ts b/desktop/cmp/pinpad/impl/PinPad.ts index dd9b4c02d7..e173cd24d4 100644 --- a/desktop/cmp/pinpad/impl/PinPad.ts +++ b/desktop/cmp/pinpad/impl/PinPad.ts @@ -4,6 +4,7 @@ * * Copyright © 2024 Extremely Heavy Industries Inc. */ +import {ForwardedRef} from 'react'; import composeRefs from '@seznam/compose-react-refs'; import {div, frame, h1, hbox, p, span, vbox, vframe} from '@xh/hoist/cmp/layout'; import {PinPadModel} from '@xh/hoist/cmp/pinpad'; @@ -22,7 +23,7 @@ import './PinPad.scss'; */ export function pinPadImpl({model, testId}, ref) { return frame({ - ref: composeRefs(model.ref, ref), + ref: composeRefs(model.ref as ForwardedRef, ref), item: vframe({ className: 'xh-pinpad__frame', items: [header(), display(), errorDisplay(), keypad()], diff --git a/mobile/appcontainer/ToastSource.ts b/mobile/appcontainer/ToastSource.ts index d271a393a4..722de290cd 100644 --- a/mobile/appcontainer/ToastSource.ts +++ b/mobile/appcontainer/ToastSource.ts @@ -4,6 +4,7 @@ * * Copyright © 2024 Extremely Heavy Industries Inc. */ +import {ToastModel} from '@xh/hoist/appcontainer/ToastModel'; import {ToastSourceModel} from '@xh/hoist/appcontainer/ToastSourceModel'; import {hoistCmp, uses} from '@xh/hoist/core'; import {wait} from '@xh/hoist/promise'; @@ -20,7 +21,7 @@ export const toastSource = hoistCmp.factory({ model: uses(ToastSourceModel), render({model}) { - const pending = model.toastModels.filter(it => it.isOpen), + const pending: ToastModel[] = model.toastModels.filter(it => it.isOpen), next = head(pending); if (!next) return null; diff --git a/mobile/cmp/input/ButtonGroupInput.ts b/mobile/cmp/input/ButtonGroupInput.ts index 44613c75ca..69c4a7d201 100644 --- a/mobile/cmp/input/ButtonGroupInput.ts +++ b/mobile/cmp/input/ButtonGroupInput.ts @@ -14,7 +14,10 @@ import {castArray, isEmpty, without} from 'lodash'; import {Children, cloneElement, isValidElement, ReactNode} from 'react'; import './ButtonGroupInput.scss'; -export interface ButtonGroupInputProps extends HoistProps, HoistInputProps, ButtonGroupProps { +export interface ButtonGroupInputProps + extends HoistProps, + HoistInputProps, + Omit { /** * True to allow buttons to be unselected (aka inactivated). Used when enableMulti is false. * Defaults to false. diff --git a/utils/react/LayoutPropUtils.ts b/utils/react/LayoutPropUtils.ts index 539a93d2c3..761ffa9432 100644 --- a/utils/react/LayoutPropUtils.ts +++ b/utils/react/LayoutPropUtils.ts @@ -4,7 +4,7 @@ * * Copyright © 2024 Extremely Heavy Industries Inc. */ -import {HoistProps, LayoutProps} from '@xh/hoist/core'; +import {HoistProps, LayoutProps, PlainObject} from '@xh/hoist/core'; import {forOwn, isEmpty, isNumber, isString, isNil, omit, pick} from 'lodash'; /** @@ -40,7 +40,7 @@ import {forOwn, isEmpty, isNumber, isString, isNil, omit, pick} from 'lodash'; * that afforded by the underlying flexbox styles. In particular, it accepts flex and sizing props * as raw numbers rather than strings. */ -export function getLayoutProps(props: HoistProps): LayoutProps { +export function getLayoutProps(props: PlainObject): LayoutProps { // Harvest all keys of interest const ret: LayoutProps = pick(props, allKeys) as LayoutProps; From 1e7d3cf4bbf26c2b5ff297e2c4a4de20ffd05aae Mon Sep 17 00:00:00 2001 From: Colin Rudd Date: Tue, 28 May 2024 19:04:03 -0400 Subject: [PATCH 04/23] CHANGELOG.md update --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ee0942cad4..52c11bf614 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### 🐞 Bug Fixes * Restore support for `ref` prop in Hoist components written in JSX. +* Made Hoist Components limit `ref` prop only to those setup to handle it. ## 64.0.2 - 2024-05-23 From d500557963ad5624f501c13c389525e664faf0ad Mon Sep 17 00:00:00 2001 From: Colin Rudd Date: Tue, 28 May 2024 20:05:48 -0400 Subject: [PATCH 05/23] Had missed fixing up desktop buttons. Had forgotten to remove ref from ElementSpec. --- core/elem.ts | 4 ---- desktop/cmp/button/ButtonGroup.ts | 3 +++ desktop/cmp/input/ButtonGroupInput.ts | 4 +++- mobile/cmp/button/Button.ts | 3 ++- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/core/elem.ts b/core/elem.ts index 70dadbcf1e..f568f661a2 100644 --- a/core/elem.ts +++ b/core/elem.ts @@ -8,7 +8,6 @@ import {TEST_ID} from '@xh/hoist/utils/js'; import {castArray, isFunction, isNil, isPlainObject} from 'lodash'; import { createElement as reactCreateElement, - ForwardedRef, isValidElement, Key, ReactElement, @@ -55,9 +54,6 @@ export type ElementSpec

= P & { //----------------------------------- // Core React attributes //----------------------------------- - /** React Ref for this component. */ - ref?: ForwardedRef; - /** React key for this component. */ key?: Key; diff --git a/desktop/cmp/button/ButtonGroup.ts b/desktop/cmp/button/ButtonGroup.ts index aa6aba9ba9..996502db63 100644 --- a/desktop/cmp/button/ButtonGroup.ts +++ b/desktop/cmp/button/ButtonGroup.ts @@ -17,6 +17,7 @@ import '@xh/hoist/desktop/register'; import {buttonGroup as bpButtonGroup} from '@xh/hoist/kit/blueprint'; import {TEST_ID} from '@xh/hoist/utils/js'; import {splitLayoutProps} from '@xh/hoist/utils/react'; +import {ForwardedRef} from 'react'; import {SetOptional} from 'type-fest'; export interface ButtonGroupProps @@ -33,6 +34,8 @@ export interface ButtonGroupProps /** True to render in a vertical orientation. */ vertical?: boolean; + + ref?: ForwardedRef; } /** diff --git a/desktop/cmp/input/ButtonGroupInput.ts b/desktop/cmp/input/ButtonGroupInput.ts index 50ff95c700..c2317bd9a5 100644 --- a/desktop/cmp/input/ButtonGroupInput.ts +++ b/desktop/cmp/input/ButtonGroupInput.ts @@ -12,7 +12,7 @@ import {ButtonProps} from '@xh/hoist/desktop/cmp/button'; import {throwIf, warnIf, withDefault} from '@xh/hoist/utils/js'; import {getLayoutProps, getNonLayoutProps} from '@xh/hoist/utils/react'; import {castArray, filter, isEmpty, without} from 'lodash'; -import {Children, cloneElement, isValidElement} from 'react'; +import {Children, cloneElement, ForwardedRef, isValidElement} from 'react'; export interface ButtonGroupInputProps extends Omit, 'onChange'>, @@ -31,6 +31,8 @@ export interface ButtonGroupInputProps /** True to create outlined-style buttons. */ outlined?: boolean; + + ref?: ForwardedRef; } /** diff --git a/mobile/cmp/button/Button.ts b/mobile/cmp/button/Button.ts index 8d7b2af334..1842c84ce1 100644 --- a/mobile/cmp/button/Button.ts +++ b/mobile/cmp/button/Button.ts @@ -10,7 +10,7 @@ import {button as onsenButton} from '@xh/hoist/kit/onsen'; import '@xh/hoist/mobile/register'; import {splitLayoutProps} from '@xh/hoist/utils/react'; import classNames from 'classnames'; -import {ReactNode, ReactElement, MouseEvent} from 'react'; +import {ReactNode, ReactElement, MouseEvent, ForwardedRef} from 'react'; import './Button.scss'; export interface ButtonProps @@ -28,6 +28,7 @@ export interface ButtonProps value?: any; modifier?: string; tabIndex?: number; + ref?: ForwardedRef; } /** From 53b2506a83ea6e4b647f4b3498baa4017f8b5dd8 Mon Sep 17 00:00:00 2001 From: Colin Rudd Date: Tue, 4 Jun 2024 12:00:00 -0400 Subject: [PATCH 06/23] Fix ref type on ButtonGroup.ts and ButtonGroupInput.ts --- desktop/cmp/button/ButtonGroup.ts | 2 +- desktop/cmp/input/ButtonGroupInput.ts | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/desktop/cmp/button/ButtonGroup.ts b/desktop/cmp/button/ButtonGroup.ts index 996502db63..50f4769695 100644 --- a/desktop/cmp/button/ButtonGroup.ts +++ b/desktop/cmp/button/ButtonGroup.ts @@ -35,7 +35,7 @@ export interface ButtonGroupProps /** True to render in a vertical orientation. */ vertical?: boolean; - ref?: ForwardedRef; + ref?: ForwardedRef; } /** diff --git a/desktop/cmp/input/ButtonGroupInput.ts b/desktop/cmp/input/ButtonGroupInput.ts index c2317bd9a5..b49aaf7cf1 100644 --- a/desktop/cmp/input/ButtonGroupInput.ts +++ b/desktop/cmp/input/ButtonGroupInput.ts @@ -12,10 +12,10 @@ import {ButtonProps} from '@xh/hoist/desktop/cmp/button'; import {throwIf, warnIf, withDefault} from '@xh/hoist/utils/js'; import {getLayoutProps, getNonLayoutProps} from '@xh/hoist/utils/react'; import {castArray, filter, isEmpty, without} from 'lodash'; -import {Children, cloneElement, ForwardedRef, isValidElement} from 'react'; +import {Children, cloneElement, isValidElement} from 'react'; export interface ButtonGroupInputProps - extends Omit, 'onChange'>, + extends Omit, 'onChange' | 'ref'>, HoistInputProps { /** * True to allow buttons to be unselected (aka inactivated). Defaults to false. @@ -31,8 +31,6 @@ export interface ButtonGroupInputProps /** True to create outlined-style buttons. */ outlined?: boolean; - - ref?: ForwardedRef; } /** From 9a7f3c8d72a92d869fa1d2354e4ddf5b14ffa0d0 Mon Sep 17 00:00:00 2001 From: Colin Rudd Date: Tue, 4 Jun 2024 12:00:50 -0400 Subject: [PATCH 07/23] Setup PinPadProps --- cmp/pinpad/PinPad.ts | 8 ++++++-- desktop/cmp/pinpad/impl/PinPad.ts | 28 +++++++++++++++------------- mobile/cmp/pinpad/impl/PinPad.ts | 6 +++--- 3 files changed, 24 insertions(+), 18 deletions(-) diff --git a/cmp/pinpad/PinPad.ts b/cmp/pinpad/PinPad.ts index 99df78e6c5..1b5d4032e5 100644 --- a/cmp/pinpad/PinPad.ts +++ b/cmp/pinpad/PinPad.ts @@ -4,12 +4,16 @@ * * Copyright © 2024 Extremely Heavy Industries Inc. */ -import {hoistCmp, uses, XH} from '@xh/hoist/core'; +import {hoistCmp, uses, HoistProps, XH, TestSupportProps} from '@xh/hoist/core'; import {pinPadImpl as desktopPinPadImpl} from '@xh/hoist/dynamics/desktop'; import {pinPadImpl as mobilePinPadImpl} from '@xh/hoist/dynamics/mobile'; +import {ForwardedRef} from 'react'; import {PinPadModel} from './PinPadModel'; +export interface PinPadProps extends HoistProps, TestSupportProps { + ref?: ForwardedRef; +} /** * Displays a prompt used to get obtain a PIN from the user. * @@ -17,7 +21,7 @@ import {PinPadModel} from './PinPadModel'; * * @see PinPadModel */ -export const [PinPad, pinPad] = hoistCmp.withFactory({ +export const [PinPad, pinPad] = hoistCmp.withFactory({ displayName: 'PinPad', model: uses(PinPadModel), className: 'xh-pinpad', diff --git a/desktop/cmp/pinpad/impl/PinPad.ts b/desktop/cmp/pinpad/impl/PinPad.ts index e173cd24d4..1b07bdc6f7 100644 --- a/desktop/cmp/pinpad/impl/PinPad.ts +++ b/desktop/cmp/pinpad/impl/PinPad.ts @@ -4,11 +4,10 @@ * * Copyright © 2024 Extremely Heavy Industries Inc. */ -import {ForwardedRef} from 'react'; import composeRefs from '@seznam/compose-react-refs'; import {div, frame, h1, hbox, p, span, vbox, vframe} from '@xh/hoist/cmp/layout'; -import {PinPadModel} from '@xh/hoist/cmp/pinpad'; -import {hoistCmp} from '@xh/hoist/core'; +import {PinPadModel, PinPadProps} from '@xh/hoist/cmp/pinpad'; +import {hoistCmp, uses} from '@xh/hoist/core'; import {button} from '@xh/hoist/desktop/cmp/button'; import '@xh/hoist/desktop/register'; import {Icon} from '@xh/hoist/icon/Icon'; @@ -21,16 +20,19 @@ import './PinPad.scss'; * * @internal */ -export function pinPadImpl({model, testId}, ref) { - return frame({ - ref: composeRefs(model.ref as ForwardedRef, ref), - item: vframe({ - className: 'xh-pinpad__frame', - items: [header(), display(), errorDisplay(), keypad()], - testId - }) - }); -} +export const pinPadImpl = hoistCmp.factory({ + model: uses(PinPadModel), + render({model, testId}, ref) { + return frame({ + ref: composeRefs(model.ref, ref), + item: vframe({ + className: 'xh-pinpad__frame', + items: [header(), display(), errorDisplay(), keypad()], + testId + }) + }); + } +}); const header = hoistCmp.factory(({model}) => div({ diff --git a/mobile/cmp/pinpad/impl/PinPad.ts b/mobile/cmp/pinpad/impl/PinPad.ts index a7392f173e..2abea359c7 100644 --- a/mobile/cmp/pinpad/impl/PinPad.ts +++ b/mobile/cmp/pinpad/impl/PinPad.ts @@ -6,7 +6,7 @@ */ import composeRefs from '@seznam/compose-react-refs'; import {div, frame, h1, hbox, p, span, vbox, vframe} from '@xh/hoist/cmp/layout'; -import {PinPadModel} from '@xh/hoist/cmp/pinpad'; +import {PinPadModel, PinPadProps} from '@xh/hoist/cmp/pinpad'; import {hoistCmp, uses} from '@xh/hoist/core'; import {Icon} from '@xh/hoist/icon/Icon'; import {button} from '@xh/hoist/mobile/cmp/button'; @@ -20,11 +20,11 @@ import './PinPad.scss'; * * @internal */ -export const pinPadImpl = hoistCmp.factory({ +export const pinPadImpl = hoistCmp.factory({ model: uses(PinPadModel), render({model}, ref) { return frame({ - ref: composeRefs(ref, model.ref), + ref: composeRefs(model.ref, ref), item: vframe({ className: 'xh-pinpad__frame', items: [header(), display(), errorDisplay(), keypad()] From 722758db609140eafdcb657ab8c1cf6c373e0af1 Mon Sep 17 00:00:00 2001 From: Colin Rudd Date: Tue, 4 Jun 2024 13:58:26 -0400 Subject: [PATCH 08/23] Minor changes from CR with Lee and Greg. --- CHANGELOG.md | 5 ++--- cmp/input/HoistInputProps.ts | 1 + cmp/pinpad/PinPad.ts | 2 +- cmp/pinpad/PinPadModel.ts | 2 +- core/HoistComponent.ts | 6 +++--- utils/react/LayoutPropUtils.ts | 6 +++--- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9dc4fc4a1c..331beb97e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,9 @@ ## 65.0-SNAPSHOT - Unreleased -### 🐞 Bug Fixes +### ⚙️ Technical -* Restore support for `ref` prop in Hoist components written in JSX. -* Made Hoist Components limit `ref` prop only to those setup to handle it. +* Typescript: Narrow typing of `ref` prop, and improve `ref` typing in JSX. ## 64.0.3 - 2024-05-31 diff --git a/cmp/input/HoistInputProps.ts b/cmp/input/HoistInputProps.ts index 93c554ea7e..7c03ecfed9 100644 --- a/cmp/input/HoistInputProps.ts +++ b/cmp/input/HoistInputProps.ts @@ -33,5 +33,6 @@ export interface HoistInputProps extends TestSupportProps { /** Value of the control, if provided directly. */ value?: any; + /** Ref to implementing control element */ ref?: ForwardedRef; } diff --git a/cmp/pinpad/PinPad.ts b/cmp/pinpad/PinPad.ts index 1b5d4032e5..800c48e3fa 100644 --- a/cmp/pinpad/PinPad.ts +++ b/cmp/pinpad/PinPad.ts @@ -4,10 +4,10 @@ * * Copyright © 2024 Extremely Heavy Industries Inc. */ +import {ForwardedRef} from 'react'; import {hoistCmp, uses, HoistProps, XH, TestSupportProps} from '@xh/hoist/core'; import {pinPadImpl as desktopPinPadImpl} from '@xh/hoist/dynamics/desktop'; import {pinPadImpl as mobilePinPadImpl} from '@xh/hoist/dynamics/mobile'; -import {ForwardedRef} from 'react'; import {PinPadModel} from './PinPadModel'; diff --git a/cmp/pinpad/PinPadModel.ts b/cmp/pinpad/PinPadModel.ts index 352be9a97e..80d59fa9ed 100644 --- a/cmp/pinpad/PinPadModel.ts +++ b/cmp/pinpad/PinPadModel.ts @@ -27,7 +27,7 @@ export class PinPadModel extends HoistModel { @bindable subHeaderText: string; @bindable errorText: string; - ref = createObservableRef(); + ref = createObservableRef(); @observable private _enteredDigits: number[]; diff --git a/core/HoistComponent.ts b/core/HoistComponent.ts index 9ac624de8a..5a482a2d28 100644 --- a/core/HoistComponent.ts +++ b/core/HoistComponent.ts @@ -47,9 +47,9 @@ import { /** * Type representing props passed to a HoistComponent's render function. * - * This type removes from its base type several props that are pulled out by the HoistComponent itself and - * not provided to the render function. Ref is passed as a forwarded ref as the second argument - * to the render function. + * This type removes from its base type several properties that are pulled out by the HoistComponent itself and + * not provided to the render function. `modelConfig` and `modelRef` are resolved into the `model` property. + * `ref` is passed as the second argument to the render function. */ export type RenderPropsOf

= Omit; diff --git a/utils/react/LayoutPropUtils.ts b/utils/react/LayoutPropUtils.ts index 761ffa9432..1978570718 100644 --- a/utils/react/LayoutPropUtils.ts +++ b/utils/react/LayoutPropUtils.ts @@ -4,7 +4,7 @@ * * Copyright © 2024 Extremely Heavy Industries Inc. */ -import {HoistProps, LayoutProps, PlainObject} from '@xh/hoist/core'; +import {LayoutProps, PlainObject} from '@xh/hoist/core'; import {forOwn, isEmpty, isNumber, isString, isNil, omit, pick} from 'lodash'; /** @@ -66,14 +66,14 @@ export function getLayoutProps(props: PlainObject): LayoutProps { /** * Return all non-layout related props found in props. */ -export function getNonLayoutProps(props: T): T { +export function getNonLayoutProps(props: T): T { return omit(props, allKeys) as T; } /** * Split a set of props into layout and non-layout props. */ -export function splitLayoutProps(props: T): [LayoutProps, T] { +export function splitLayoutProps(props: T): [LayoutProps, T] { const layoutProps = getLayoutProps(props); return [layoutProps, isEmpty(layoutProps) ? props : getNonLayoutProps(props)]; } From 99bd39abc643b9d3ad84f616a16c82052b1dfb96 Mon Sep 17 00:00:00 2001 From: Greg Solomon Date: Tue, 4 Jun 2024 15:46:18 -0400 Subject: [PATCH 09/23] Make `ref` argument passed to `render` generic + other ref-related type errors --- cmp/chart/Chart.ts | 3 +++ cmp/grid/Grid.ts | 2 +- cmp/layout/TileFrame.ts | 6 ++++-- core/HoistComponent.ts | 9 ++++++--- desktop/cmp/button/ColChooserButton.ts | 5 ++++- desktop/cmp/button/ZoneMapperButton.ts | 5 ++++- desktop/cmp/dash/canvas/DashCanvas.ts | 5 ++++- desktop/cmp/dash/container/DashContainer.ts | 6 ++++-- desktop/cmp/grouping/GroupingChooser.ts | 5 ++++- desktop/cmp/panel/impl/ResizeContainer.ts | 10 +++++++--- desktop/cmp/treemap/TreeMap.ts | 5 ++++- 11 files changed, 45 insertions(+), 16 deletions(-) diff --git a/cmp/chart/Chart.ts b/cmp/chart/Chart.ts index 850a6a1ab4..f2aa2a263f 100644 --- a/cmp/chart/Chart.ts +++ b/cmp/chart/Chart.ts @@ -4,6 +4,7 @@ * * Copyright © 2024 Extremely Heavy Industries Inc. */ +import {ForwardedRef} from 'react'; import composeRefs from '@seznam/compose-react-refs'; import {box, div} from '@xh/hoist/cmp/layout'; import { @@ -48,6 +49,8 @@ export interface ChartProps extends HoistProps, LayoutProps, TestSup * dimensions to take up all available space. */ aspectRatio?: number; + + ref?: ForwardedRef; } /** diff --git a/cmp/grid/Grid.ts b/cmp/grid/Grid.ts index 4d96e77810..de26619595 100644 --- a/cmp/grid/Grid.ts +++ b/cmp/grid/Grid.ts @@ -148,7 +148,7 @@ export class GridLocalModel extends HoistModel { @lookup(GridModel) private model: GridModel; agOptions: GridOptions; - viewRef = createObservableRef(); + viewRef = createObservableRef(); private rowKeyNavSupport: RowKeyNavSupport; private prevRs: RecordSet; diff --git a/cmp/layout/TileFrame.ts b/cmp/layout/TileFrame.ts index 64b8fa2d0d..2702f4bc90 100644 --- a/cmp/layout/TileFrame.ts +++ b/cmp/layout/TileFrame.ts @@ -7,7 +7,7 @@ import {hoistCmp, useLocalModel, HoistModel, BoxProps, HoistProps} from '@xh/hoist/core'; import {frame, box} from '@xh/hoist/cmp/layout'; import {useOnResize} from '@xh/hoist/utils/react'; -import {useState, useLayoutEffect} from 'react'; +import {useState, useLayoutEffect, ForwardedRef} from 'react'; import {minBy, isEqual} from 'lodash'; import composeRefs from '@seznam/compose-react-refs'; import {Children} from 'react'; @@ -44,6 +44,8 @@ export interface TileFrameProps extends HoistProps, BoxProps { tileWidth: number; tileHeight: number; }) => any; + + ref?: ForwardedRef; } /** @@ -55,7 +57,7 @@ export interface TileFrameProps extends HoistProps, BoxProps { * stable layouts. These should be used judiciously, however, as each constraint limits the ability * of the TileFrame to fill its available space. */ -export const [TileFrame, tileFrame] = hoistCmp.withFactory({ +export const [TileFrame, tileFrame] = hoistCmp.withFactory({ displayName: 'TileFrame', memo: false, observer: false, diff --git a/core/HoistComponent.ts b/core/HoistComponent.ts index 5a482a2d28..aab6fee39d 100644 --- a/core/HoistComponent.ts +++ b/core/HoistComponent.ts @@ -58,11 +58,14 @@ export type RenderPropsOf

= Omit = - | ((props: RenderPropsOf

, ref?: ForwardedRef) => ReactNode) +export type ComponentConfig< + P extends HoistProps, + R = P extends {ref?: ForwardedRef} ? ForwardedRef : never +> = + | ((props: RenderPropsOf

, ref?: R) => ReactNode) | { /** Render function defining the component. */ - render(props: RenderPropsOf

, ref?: ForwardedRef): ReactNode; + render(props: RenderPropsOf

, ref?: R): ReactNode; /** * Spec defining the model to be rendered by this component. diff --git a/desktop/cmp/button/ColChooserButton.ts b/desktop/cmp/button/ColChooserButton.ts index 5b04354a70..5c64b47049 100644 --- a/desktop/cmp/button/ColChooserButton.ts +++ b/desktop/cmp/button/ColChooserButton.ts @@ -13,14 +13,17 @@ import '@xh/hoist/desktop/register'; import {Icon} from '@xh/hoist/icon'; import {popover, Position} from '@xh/hoist/kit/blueprint'; import {logError, stopPropagation, withDefault} from '@xh/hoist/utils/js'; +import {ForwardedRef} from 'react'; import {button, ButtonProps} from './Button'; -export interface ColChooserButtonProps extends ButtonProps { +export interface ColChooserButtonProps extends Omit { /** GridModel of the grid for which this button should show a chooser. */ gridModel?: GridModel; /** Position for chooser popover, as per Blueprint docs. */ popoverPosition?: Position; + + ref?: ForwardedRef; } /** diff --git a/desktop/cmp/button/ZoneMapperButton.ts b/desktop/cmp/button/ZoneMapperButton.ts index 9abdc339a7..ce30bf0bd2 100644 --- a/desktop/cmp/button/ZoneMapperButton.ts +++ b/desktop/cmp/button/ZoneMapperButton.ts @@ -13,14 +13,17 @@ import {zoneMapper} from '@xh/hoist/desktop/cmp/zoneGrid/impl/ZoneMapper'; import {Icon} from '@xh/hoist/icon'; import {popover, Position} from '@xh/hoist/kit/blueprint'; import {logError, stopPropagation, withDefault} from '@xh/hoist/utils/js'; +import {ForwardedRef} from 'react'; import {button, ButtonProps} from './Button'; -export interface ZoneMapperButtonProps extends ButtonProps { +export interface ZoneMapperButtonProps extends Omit { /** ZoneGridModel of the grid for which this button should show a chooser. */ zoneGridModel?: ZoneGridModel; /** Position for chooser popover, as per Blueprint docs. */ popoverPosition?: Position; + + ref?: ForwardedRef; } /** diff --git a/desktop/cmp/dash/canvas/DashCanvas.ts b/desktop/cmp/dash/canvas/DashCanvas.ts index 52c50a8ccc..7db162cee9 100644 --- a/desktop/cmp/dash/canvas/DashCanvas.ts +++ b/desktop/cmp/dash/canvas/DashCanvas.ts @@ -21,6 +21,7 @@ import {Classes, overlay} from '@xh/hoist/kit/blueprint'; import {TEST_ID} from '@xh/hoist/utils/js'; import {useOnVisibleChange} from '@xh/hoist/utils/react'; import classNames from 'classnames'; +import {ForwardedRef} from 'react'; import ReactGridLayout, {WidthProvider} from 'react-grid-layout'; import {DashCanvasModel} from './DashCanvasModel'; import {dashCanvasContextMenu} from './impl/DashCanvasContextMenu'; @@ -29,7 +30,9 @@ import {dashCanvasView} from './impl/DashCanvasView'; import 'react-grid-layout/css/styles.css'; import './DashCanvas.scss'; -export type DashCanvasProps = HoistProps & TestSupportProps; +export interface DashCanvasProps extends HoistProps, TestSupportProps { + ref?: ForwardedRef; +} /** * Dashboard-style container that allows users to drag-and-drop child widgets into flexible layouts. diff --git a/desktop/cmp/dash/container/DashContainer.ts b/desktop/cmp/dash/container/DashContainer.ts index b86f34d3cd..cadf5c2995 100644 --- a/desktop/cmp/dash/container/DashContainer.ts +++ b/desktop/cmp/dash/container/DashContainer.ts @@ -17,12 +17,14 @@ import { import {mask} from '@xh/hoist/desktop/cmp/mask'; import {Classes, overlay} from '@xh/hoist/kit/blueprint'; import {useOnMount, useOnResize} from '@xh/hoist/utils/react'; -import {useContext} from 'react'; +import {ForwardedRef, useContext} from 'react'; import './DashContainer.scss'; import {DashContainerModel} from './DashContainerModel'; import {dashContainerAddViewButton} from './impl/DashContainerContextMenu'; -export type DashContainerProps = HoistProps & TestSupportProps; +export interface DashContainerProps extends HoistProps, TestSupportProps { + ref?: ForwardedRef; +} /** * Display a set of child components in accordance with a DashContainerModel. diff --git a/desktop/cmp/grouping/GroupingChooser.ts b/desktop/cmp/grouping/GroupingChooser.ts index ec134d3025..7eea5968dd 100644 --- a/desktop/cmp/grouping/GroupingChooser.ts +++ b/desktop/cmp/grouping/GroupingChooser.ts @@ -19,8 +19,9 @@ import {splitLayoutProps} from '@xh/hoist/utils/react'; import classNames from 'classnames'; import {compact, isEmpty, sortBy} from 'lodash'; import './GroupingChooser.scss'; +import {ForwardedRef} from 'react'; -export interface GroupingChooserProps extends ButtonProps { +export interface GroupingChooserProps extends Omit, 'ref'> { /** Text to represent empty state (i.e. value = null or []) */ emptyText?: string; @@ -38,6 +39,8 @@ export interface GroupingChooserProps extends ButtonProps /** True (default) to style target button as an input field - blends better in toolbars. */ styleButtonAsInput?: boolean; + + ref?: ForwardedRef; } /** diff --git a/desktop/cmp/panel/impl/ResizeContainer.ts b/desktop/cmp/panel/impl/ResizeContainer.ts index 06db1a5990..dc4f37eb04 100644 --- a/desktop/cmp/panel/impl/ResizeContainer.ts +++ b/desktop/cmp/panel/impl/ResizeContainer.ts @@ -6,14 +6,18 @@ */ import composeRefs from '@seznam/compose-react-refs'; import {box, hbox, vbox} from '@xh/hoist/cmp/layout'; -import {hoistCmp, useContextModel} from '@xh/hoist/core'; +import {hoistCmp, HoistProps, TestSupportProps, useContextModel} from '@xh/hoist/core'; import {isString} from 'lodash'; -import {Children} from 'react'; +import {Children, ForwardedRef} from 'react'; import {PanelModel} from '../PanelModel'; import {dragger} from './dragger/Dragger'; import {splitter} from './Splitter'; -export const resizeContainer = hoistCmp.factory({ +interface ResizeContainerProps extends HoistProps, TestSupportProps { + ref?: ForwardedRef; +} + +export const resizeContainer = hoistCmp.factory({ displayName: 'ResizeContainer', model: false, className: 'xh-resizable', diff --git a/desktop/cmp/treemap/TreeMap.ts b/desktop/cmp/treemap/TreeMap.ts index 77e5cad04f..20463d32c7 100644 --- a/desktop/cmp/treemap/TreeMap.ts +++ b/desktop/cmp/treemap/TreeMap.ts @@ -34,9 +34,12 @@ import equal from 'fast-deep-equal'; import {assign, cloneDeep, debounce, isFunction, merge, omit} from 'lodash'; import './TreeMap.scss'; +import {ForwardedRef} from 'react'; import {TreeMapModel} from './TreeMapModel'; -export interface TreeMapProps extends HoistProps, LayoutProps, TestSupportProps {} +export interface TreeMapProps extends HoistProps, LayoutProps, TestSupportProps { + ref?: ForwardedRef; +} /** * Component for rendering a TreeMap. From 68e6cf67f1b1a82cf9d01a425b6398ace1e99d76 Mon Sep 17 00:00:00 2001 From: Greg Solomon Date: Wed, 5 Jun 2024 08:33:38 -0400 Subject: [PATCH 10/23] Improve type handling for refs --- cmp/ag-grid/AgGrid.ts | 4 +++- cmp/chart/Chart.ts | 10 ++++++---- cmp/dataview/DataView.ts | 10 ++++++---- cmp/grid/Grid.ts | 10 ++++++---- cmp/input/HoistInputProps.ts | 7 ++----- cmp/layout/TileFrame.ts | 6 ++---- cmp/pinpad/PinPad.ts | 9 +++++---- cmp/zoneGrid/ZoneGrid.ts | 7 ++++++- core/HoistComponent.ts | 9 +++++---- core/HoistProps.ts | 7 +++---- desktop/cmp/button/Button.ts | 7 +++---- desktop/cmp/button/ButtonGroup.ts | 7 +++---- desktop/cmp/button/ColChooserButton.ts | 8 ++++---- desktop/cmp/button/ZoneMapperButton.ts | 8 ++++---- desktop/cmp/dash/canvas/DashCanvas.ts | 8 ++++---- desktop/cmp/dash/container/DashContainer.ts | 8 ++++---- desktop/cmp/filter/FilterChooser.ts | 7 +++++-- desktop/cmp/grouping/GroupingChooser.ts | 8 ++++---- desktop/cmp/input/ButtonGroupInput.ts | 4 ++-- desktop/cmp/input/CodeInput.ts | 4 ++-- desktop/cmp/input/Select.ts | 4 ++-- desktop/cmp/input/Slider.ts | 4 ++-- desktop/cmp/panel/impl/ResizeContainer.ts | 10 ++++------ desktop/cmp/treemap/TreeMap.ts | 10 ++++++---- mobile/cmp/button/Button.ts | 6 +++--- mobile/cmp/error/ErrorMessage.ts | 4 ++-- mobile/cmp/grid/impl/ColChooser.ts | 5 +++-- mobile/cmp/header/AppBar.ts | 4 ++-- mobile/cmp/input/ButtonGroupInput.ts | 4 ++-- mobile/cmp/input/DateInput.ts | 4 ++-- mobile/cmp/input/Label.ts | 3 ++- mobile/cmp/input/Select.ts | 6 +++--- mobile/cmp/input/TextArea.ts | 3 ++- mobile/cmp/input/TextInput.ts | 3 ++- mobile/cmp/loadingindicator/LoadingIndicator.ts | 3 ++- mobile/cmp/mask/Mask.ts | 4 ++-- mobile/cmp/menu/impl/Menu.ts | 11 +++++++++-- 37 files changed, 129 insertions(+), 107 deletions(-) diff --git a/cmp/ag-grid/AgGrid.ts b/cmp/ag-grid/AgGrid.ts index e81b0b2dc2..bbe149b787 100644 --- a/cmp/ag-grid/AgGrid.ts +++ b/cmp/ag-grid/AgGrid.ts @@ -23,13 +23,15 @@ import {splitLayoutProps} from '@xh/hoist/utils/react'; import classNames from 'classnames'; import {isNil} from 'lodash'; import './AgGrid.scss'; +import {RefAttributes} from 'react'; import {AgGridModel} from './AgGridModel'; export interface AgGridProps extends HoistProps, GridOptions, LayoutProps, - TestSupportProps {} + TestSupportProps, + RefAttributes {} /** * Minimal wrapper for AgGridReact, supporting direct use of the ag-Grid component with limited diff --git a/cmp/chart/Chart.ts b/cmp/chart/Chart.ts index f2aa2a263f..92f5e64173 100644 --- a/cmp/chart/Chart.ts +++ b/cmp/chart/Chart.ts @@ -4,7 +4,6 @@ * * Copyright © 2024 Extremely Heavy Industries Inc. */ -import {ForwardedRef} from 'react'; import composeRefs from '@seznam/compose-react-refs'; import {box, div} from '@xh/hoist/cmp/layout'; import { @@ -31,6 +30,7 @@ import { useOnVisibleChange } from '@xh/hoist/utils/react'; import {assign, castArray, cloneDeep, forOwn, isEqual, isPlainObject, merge, omit} from 'lodash'; +import {RefAttributes} from 'react'; import {placeholder} from '../layout'; import './Chart.scss'; import {ChartModel} from './ChartModel'; @@ -42,15 +42,17 @@ import {LightTheme} from './theme/Light'; installZoomoutGesture(Highcharts); installCopyToClipboard(Highcharts); -export interface ChartProps extends HoistProps, LayoutProps, TestSupportProps { +export interface ChartProps + extends HoistProps, + LayoutProps, + TestSupportProps, + RefAttributes { /** * Ratio of width-to-height of displayed chart. If defined and greater than 0, the chart will * respect this ratio within the available space. Otherwise, the chart will stretch on both * dimensions to take up all available space. */ aspectRatio?: number; - - ref?: ForwardedRef; } /** diff --git a/cmp/dataview/DataView.ts b/cmp/dataview/DataView.ts index 4166020950..b418a25b30 100644 --- a/cmp/dataview/DataView.ts +++ b/cmp/dataview/DataView.ts @@ -4,7 +4,6 @@ * * Copyright © 2024 Extremely Heavy Industries Inc. */ -import {ForwardedRef} from 'react'; import {AgGrid} from '@xh/hoist/cmp/ag-grid'; import {grid} from '@xh/hoist/cmp/grid'; import { @@ -22,9 +21,14 @@ import type {GridOptions} from '@xh/hoist/kit/ag-grid'; import {splitLayoutProps} from '@xh/hoist/utils/react'; import {isFunction, merge} from 'lodash'; import './DataView.scss'; +import {RefAttributes} from 'react'; import {DataViewModel} from './DataViewModel'; -export interface DataViewProps extends HoistProps, LayoutProps, TestSupportProps { +export interface DataViewProps + extends HoistProps, + LayoutProps, + TestSupportProps, + RefAttributes { /** * Options for ag-Grid's API. * @@ -35,8 +39,6 @@ export interface DataViewProps extends HoistProps, LayoutProps, T * Note that changes to these options after the component's initial render will be ignored. */ agOptions?: GridOptions; - - ref?: ForwardedRef; } /** diff --git a/cmp/grid/Grid.ts b/cmp/grid/Grid.ts index de26619595..f291104712 100644 --- a/cmp/grid/Grid.ts +++ b/cmp/grid/Grid.ts @@ -4,7 +4,6 @@ * * Copyright © 2024 Extremely Heavy Industries Inc. */ -import {ForwardedRef} from 'react'; import composeRefs from '@seznam/compose-react-refs'; import {agGrid, AgGrid} from '@xh/hoist/cmp/ag-grid'; import {getTreeStyleClasses} from '@xh/hoist/cmp/grid'; @@ -47,12 +46,17 @@ import {createObservableRef, getLayoutProps} from '@xh/hoist/utils/react'; import classNames from 'classnames'; import {debounce, isEmpty, isEqual, isNil, max, maxBy, merge} from 'lodash'; import './Grid.scss'; +import {RefAttributes} from 'react'; import {GridModel} from './GridModel'; import {columnGroupHeader} from './impl/ColumnGroupHeader'; import {columnHeader} from './impl/ColumnHeader'; import {RowKeyNavSupport} from './impl/RowKeyNavSupport'; -export interface GridProps extends HoistProps, LayoutProps, TestSupportProps { +export interface GridProps + extends HoistProps, + LayoutProps, + TestSupportProps, + RefAttributes { /** * Options for ag-Grid's API. * @@ -69,8 +73,6 @@ export interface GridProps extends HoistProps, LayoutProps, TestSuppo * event after running its internal handler to associate the ag-Grid APIs with its model. */ onGridReady?: (e: GridReadyEvent) => void; - - ref?: ForwardedRef; } /** diff --git a/cmp/input/HoistInputProps.ts b/cmp/input/HoistInputProps.ts index 7c03ecfed9..49a92e039e 100644 --- a/cmp/input/HoistInputProps.ts +++ b/cmp/input/HoistInputProps.ts @@ -6,9 +6,9 @@ */ import {TestSupportProps} from '@xh/hoist/core'; -import {ForwardedRef} from 'react'; +import {RefAttributes} from 'react'; -export interface HoistInputProps extends TestSupportProps { +export interface HoistInputProps extends TestSupportProps, RefAttributes { /** * Field or model property name from which this component should read and write its value * in controlled mode. Can be set by parent FormField. @@ -32,7 +32,4 @@ export interface HoistInputProps extends TestSupportProps { /** Value of the control, if provided directly. */ value?: any; - - /** Ref to implementing control element */ - ref?: ForwardedRef; } diff --git a/cmp/layout/TileFrame.ts b/cmp/layout/TileFrame.ts index 2702f4bc90..71bc033d7e 100644 --- a/cmp/layout/TileFrame.ts +++ b/cmp/layout/TileFrame.ts @@ -7,14 +7,14 @@ import {hoistCmp, useLocalModel, HoistModel, BoxProps, HoistProps} from '@xh/hoist/core'; import {frame, box} from '@xh/hoist/cmp/layout'; import {useOnResize} from '@xh/hoist/utils/react'; -import {useState, useLayoutEffect, ForwardedRef} from 'react'; +import {useState, useLayoutEffect, RefAttributes} from 'react'; import {minBy, isEqual} from 'lodash'; import composeRefs from '@seznam/compose-react-refs'; import {Children} from 'react'; import './TileFrame.scss'; -export interface TileFrameProps extends HoistProps, BoxProps { +export interface TileFrameProps extends HoistProps, BoxProps, RefAttributes { /** * Desired tile width / height ratio (i.e. desiredRatio: 2 == twice as wide as tall). * The container will strive to meet this ratio, but the final ratio may vary. @@ -44,8 +44,6 @@ export interface TileFrameProps extends HoistProps, BoxProps { tileWidth: number; tileHeight: number; }) => any; - - ref?: ForwardedRef; } /** diff --git a/cmp/pinpad/PinPad.ts b/cmp/pinpad/PinPad.ts index 800c48e3fa..83de3af7c7 100644 --- a/cmp/pinpad/PinPad.ts +++ b/cmp/pinpad/PinPad.ts @@ -4,16 +4,17 @@ * * Copyright © 2024 Extremely Heavy Industries Inc. */ -import {ForwardedRef} from 'react'; import {hoistCmp, uses, HoistProps, XH, TestSupportProps} from '@xh/hoist/core'; import {pinPadImpl as desktopPinPadImpl} from '@xh/hoist/dynamics/desktop'; import {pinPadImpl as mobilePinPadImpl} from '@xh/hoist/dynamics/mobile'; +import {RefAttributes} from 'react'; import {PinPadModel} from './PinPadModel'; -export interface PinPadProps extends HoistProps, TestSupportProps { - ref?: ForwardedRef; -} +export interface PinPadProps + extends HoistProps, + TestSupportProps, + RefAttributes {} /** * Displays a prompt used to get obtain a PIN from the user. * diff --git a/cmp/zoneGrid/ZoneGrid.ts b/cmp/zoneGrid/ZoneGrid.ts index b363b61536..e23b96ca3d 100644 --- a/cmp/zoneGrid/ZoneGrid.ts +++ b/cmp/zoneGrid/ZoneGrid.ts @@ -11,10 +11,15 @@ import {zoneMapper as desktopZoneMapper} from '@xh/hoist/dynamics/desktop'; import {zoneMapper as mobileZoneMapper} from '@xh/hoist/dynamics/mobile'; import {GridOptions} from '@xh/hoist/kit/ag-grid'; import {splitLayoutProps} from '@xh/hoist/utils/react'; +import {RefAttributes} from 'react'; import {ZoneGridModel} from './ZoneGridModel'; import './ZoneGrid.scss'; -export interface ZoneGridProps extends HoistProps, LayoutProps, TestSupportProps { +export interface ZoneGridProps + extends HoistProps, + LayoutProps, + TestSupportProps, + RefAttributes { /** * Options for ag-Grid's API. * diff --git a/core/HoistComponent.ts b/core/HoistComponent.ts index aab6fee39d..b94c4c5efb 100644 --- a/core/HoistComponent.ts +++ b/core/HoistComponent.ts @@ -41,7 +41,8 @@ import { useRef, createElement, FunctionComponent, - useDebugValue + useDebugValue, + RefAttributes } from 'react'; /** @@ -60,12 +61,12 @@ export type RenderPropsOf

= Omit} ? ForwardedRef : never + R = P extends RefAttributes ? R : unknown > = - | ((props: RenderPropsOf

, ref?: R) => ReactNode) + | ((props: RenderPropsOf

, ref?: ForwardedRef) => ReactNode) | { /** Render function defining the component. */ - render(props: RenderPropsOf

, ref?: R): ReactNode; + render(props: RenderPropsOf

, ref?: ForwardedRef): ReactNode; /** * Spec defining the model to be rendered by this component. diff --git a/core/HoistProps.ts b/core/HoistProps.ts index 07f4aa267c..c1c45fbccb 100644 --- a/core/HoistProps.ts +++ b/core/HoistProps.ts @@ -6,7 +6,7 @@ */ import {HoistModel} from '@xh/hoist/core'; import {Property} from 'csstype'; -import {CSSProperties, HTMLAttributes, ReactNode, Ref, ForwardedRef} from 'react'; +import {CSSProperties, HTMLAttributes, ReactNode, Ref, RefAttributes} from 'react'; /** * Props interface for Hoist Components. @@ -66,9 +66,8 @@ export interface DefaultHoistProps extends Ho export interface BoxProps extends LayoutProps, TestSupportProps, - Omit, 'onChange' | 'contextMenu'> { - ref?: ForwardedRef; -} + Omit, 'onChange' | 'contextMenu'>, + RefAttributes {} /** * Props for Components that accept standard HTML `style` attributes. diff --git a/desktop/cmp/button/Button.ts b/desktop/cmp/button/Button.ts index 90b3998407..916223fbea 100644 --- a/desktop/cmp/button/Button.ts +++ b/desktop/cmp/button/Button.ts @@ -19,7 +19,7 @@ import {button as bpButton} from '@xh/hoist/kit/blueprint'; import {TEST_ID, withDefault} from '@xh/hoist/utils/js'; import {splitLayoutProps} from '@xh/hoist/utils/react'; import classNames from 'classnames'; -import {ForwardedRef, ReactElement, ReactNode} from 'react'; +import {ReactElement, ReactNode, RefAttributes} from 'react'; import './Button.scss'; export interface ButtonProps @@ -27,7 +27,8 @@ export interface ButtonProps StyleProps, LayoutProps, TestSupportProps, - Omit { + Omit, + RefAttributes { active?: boolean; autoFocus?: boolean; disabled?: boolean; @@ -44,8 +45,6 @@ export interface ButtonProps /** Alias for title. */ tooltip?: string; - - ref?: ForwardedRef; } /** diff --git a/desktop/cmp/button/ButtonGroup.ts b/desktop/cmp/button/ButtonGroup.ts index 50f4769695..c898459187 100644 --- a/desktop/cmp/button/ButtonGroup.ts +++ b/desktop/cmp/button/ButtonGroup.ts @@ -17,7 +17,7 @@ import '@xh/hoist/desktop/register'; import {buttonGroup as bpButtonGroup} from '@xh/hoist/kit/blueprint'; import {TEST_ID} from '@xh/hoist/utils/js'; import {splitLayoutProps} from '@xh/hoist/utils/react'; -import {ForwardedRef} from 'react'; +import {RefAttributes} from 'react'; import {SetOptional} from 'type-fest'; export interface ButtonGroupProps @@ -25,7 +25,8 @@ export interface ButtonGroupProps LayoutProps, StyleProps, TestSupportProps, - SetOptional, 'children'> { + SetOptional, 'children'>, + RefAttributes { /** True to have all buttons fill available width equally. */ fill?: boolean; @@ -34,8 +35,6 @@ export interface ButtonGroupProps /** True to render in a vertical orientation. */ vertical?: boolean; - - ref?: ForwardedRef; } /** diff --git a/desktop/cmp/button/ColChooserButton.ts b/desktop/cmp/button/ColChooserButton.ts index 5c64b47049..0d84948dee 100644 --- a/desktop/cmp/button/ColChooserButton.ts +++ b/desktop/cmp/button/ColChooserButton.ts @@ -13,17 +13,17 @@ import '@xh/hoist/desktop/register'; import {Icon} from '@xh/hoist/icon'; import {popover, Position} from '@xh/hoist/kit/blueprint'; import {logError, stopPropagation, withDefault} from '@xh/hoist/utils/js'; -import {ForwardedRef} from 'react'; +import {RefAttributes} from 'react'; import {button, ButtonProps} from './Button'; -export interface ColChooserButtonProps extends Omit { +export interface ColChooserButtonProps + extends Omit, + RefAttributes { /** GridModel of the grid for which this button should show a chooser. */ gridModel?: GridModel; /** Position for chooser popover, as per Blueprint docs. */ popoverPosition?: Position; - - ref?: ForwardedRef; } /** diff --git a/desktop/cmp/button/ZoneMapperButton.ts b/desktop/cmp/button/ZoneMapperButton.ts index ce30bf0bd2..043a792113 100644 --- a/desktop/cmp/button/ZoneMapperButton.ts +++ b/desktop/cmp/button/ZoneMapperButton.ts @@ -13,17 +13,17 @@ import {zoneMapper} from '@xh/hoist/desktop/cmp/zoneGrid/impl/ZoneMapper'; import {Icon} from '@xh/hoist/icon'; import {popover, Position} from '@xh/hoist/kit/blueprint'; import {logError, stopPropagation, withDefault} from '@xh/hoist/utils/js'; -import {ForwardedRef} from 'react'; +import {RefAttributes} from 'react'; import {button, ButtonProps} from './Button'; -export interface ZoneMapperButtonProps extends Omit { +export interface ZoneMapperButtonProps + extends Omit, + RefAttributes { /** ZoneGridModel of the grid for which this button should show a chooser. */ zoneGridModel?: ZoneGridModel; /** Position for chooser popover, as per Blueprint docs. */ popoverPosition?: Position; - - ref?: ForwardedRef; } /** diff --git a/desktop/cmp/dash/canvas/DashCanvas.ts b/desktop/cmp/dash/canvas/DashCanvas.ts index 7db162cee9..49bb849b2c 100644 --- a/desktop/cmp/dash/canvas/DashCanvas.ts +++ b/desktop/cmp/dash/canvas/DashCanvas.ts @@ -21,7 +21,7 @@ import {Classes, overlay} from '@xh/hoist/kit/blueprint'; import {TEST_ID} from '@xh/hoist/utils/js'; import {useOnVisibleChange} from '@xh/hoist/utils/react'; import classNames from 'classnames'; -import {ForwardedRef} from 'react'; +import {RefAttributes} from 'react'; import ReactGridLayout, {WidthProvider} from 'react-grid-layout'; import {DashCanvasModel} from './DashCanvasModel'; import {dashCanvasContextMenu} from './impl/DashCanvasContextMenu'; @@ -30,9 +30,9 @@ import {dashCanvasView} from './impl/DashCanvasView'; import 'react-grid-layout/css/styles.css'; import './DashCanvas.scss'; -export interface DashCanvasProps extends HoistProps, TestSupportProps { - ref?: ForwardedRef; -} +export type DashCanvasProps = HoistProps & + TestSupportProps & + RefAttributes; /** * Dashboard-style container that allows users to drag-and-drop child widgets into flexible layouts. diff --git a/desktop/cmp/dash/container/DashContainer.ts b/desktop/cmp/dash/container/DashContainer.ts index cadf5c2995..a0b79f64f7 100644 --- a/desktop/cmp/dash/container/DashContainer.ts +++ b/desktop/cmp/dash/container/DashContainer.ts @@ -17,14 +17,14 @@ import { import {mask} from '@xh/hoist/desktop/cmp/mask'; import {Classes, overlay} from '@xh/hoist/kit/blueprint'; import {useOnMount, useOnResize} from '@xh/hoist/utils/react'; -import {ForwardedRef, useContext} from 'react'; +import {RefAttributes, useContext} from 'react'; import './DashContainer.scss'; import {DashContainerModel} from './DashContainerModel'; import {dashContainerAddViewButton} from './impl/DashContainerContextMenu'; -export interface DashContainerProps extends HoistProps, TestSupportProps { - ref?: ForwardedRef; -} +export type DashContainerProps = HoistProps & + TestSupportProps & + RefAttributes; /** * Display a set of child components in accordance with a DashContainerModel. diff --git a/desktop/cmp/filter/FilterChooser.ts b/desktop/cmp/filter/FilterChooser.ts index ea06a4805d..287e317e1f 100644 --- a/desktop/cmp/filter/FilterChooser.ts +++ b/desktop/cmp/filter/FilterChooser.ts @@ -16,10 +16,13 @@ import {withDefault} from '@xh/hoist/utils/js'; import {splitLayoutProps} from '@xh/hoist/utils/react'; import classNames from 'classnames'; import {isEmpty, sortBy} from 'lodash'; -import {ReactElement} from 'react'; +import {ReactElement, RefAttributes} from 'react'; import './FilterChooser.scss'; -export interface FilterChooserProps extends HoistProps, LayoutProps { +export interface FilterChooserProps + extends HoistProps, + LayoutProps, + RefAttributes { /** True to focus the control on render. */ autoFocus?: boolean; /** True to disable user interaction. */ diff --git a/desktop/cmp/grouping/GroupingChooser.ts b/desktop/cmp/grouping/GroupingChooser.ts index 7eea5968dd..8dba97778f 100644 --- a/desktop/cmp/grouping/GroupingChooser.ts +++ b/desktop/cmp/grouping/GroupingChooser.ts @@ -19,9 +19,11 @@ import {splitLayoutProps} from '@xh/hoist/utils/react'; import classNames from 'classnames'; import {compact, isEmpty, sortBy} from 'lodash'; import './GroupingChooser.scss'; -import {ForwardedRef} from 'react'; +import {RefAttributes} from 'react'; -export interface GroupingChooserProps extends Omit, 'ref'> { +export interface GroupingChooserProps + extends Omit, 'ref'>, + RefAttributes { /** Text to represent empty state (i.e. value = null or []) */ emptyText?: string; @@ -39,8 +41,6 @@ export interface GroupingChooserProps extends Omit; } /** diff --git a/desktop/cmp/input/ButtonGroupInput.ts b/desktop/cmp/input/ButtonGroupInput.ts index b49aaf7cf1..086c787343 100644 --- a/desktop/cmp/input/ButtonGroupInput.ts +++ b/desktop/cmp/input/ButtonGroupInput.ts @@ -12,7 +12,7 @@ import {ButtonProps} from '@xh/hoist/desktop/cmp/button'; import {throwIf, warnIf, withDefault} from '@xh/hoist/utils/js'; import {getLayoutProps, getNonLayoutProps} from '@xh/hoist/utils/react'; import {castArray, filter, isEmpty, without} from 'lodash'; -import {Children, cloneElement, isValidElement} from 'react'; +import {Children, cloneElement, ForwardedRef, isValidElement} from 'react'; export interface ButtonGroupInputProps extends Omit, 'onChange' | 'ref'>, @@ -163,6 +163,6 @@ const cmp = hoistCmp.factory(({model, className, ...props onBlur: model.onBlur, onFocus: model.onFocus, className, - ref + ref: ref as ForwardedRef }); }); diff --git a/desktop/cmp/input/CodeInput.ts b/desktop/cmp/input/CodeInput.ts index a23067c4d2..9d4650e72d 100644 --- a/desktop/cmp/input/CodeInput.ts +++ b/desktop/cmp/input/CodeInput.ts @@ -35,7 +35,7 @@ import 'codemirror/addon/selection/mark-selection.js'; import 'codemirror/lib/codemirror.css'; import 'codemirror/theme/dracula.css'; import {compact, defaultsDeep, isEqual, isFunction} from 'lodash'; -import {ReactElement} from 'react'; +import {ForwardedRef, ReactElement} from 'react'; import {findDOMNode} from 'react-dom'; import './CodeInput.scss'; @@ -482,7 +482,7 @@ const inputCmp = hoistCmp.factory(({model, ...props}, ref) => onBlur: model.onBlur, onFocus: model.onFocus, ...props, - ref + ref: ref as ForwardedRef }) ); diff --git a/desktop/cmp/input/Select.ts b/desktop/cmp/input/Select.ts index c02b1cdeb8..fbba7d466a 100644 --- a/desktop/cmp/input/Select.ts +++ b/desktop/cmp/input/Select.ts @@ -42,7 +42,7 @@ import { keyBy, merge } from 'lodash'; -import {ReactElement, ReactNode} from 'react'; +import {ForwardedRef, ReactElement, ReactNode} from 'react'; import {components} from 'react-select'; import './Select.scss'; @@ -802,6 +802,6 @@ const cmp = hoistCmp.factory(({model, className, ...props}, re ...layoutProps, width: withDefault(width, 200), height: height, - ref + ref: ref as ForwardedRef }); }); diff --git a/desktop/cmp/input/Slider.ts b/desktop/cmp/input/Slider.ts index 483a0bfc4e..fb3fdceb46 100644 --- a/desktop/cmp/input/Slider.ts +++ b/desktop/cmp/input/Slider.ts @@ -12,7 +12,7 @@ import {rangeSlider as bpRangeSlider, slider as bpSlider} from '@xh/hoist/kit/bl import {throwIf, withDefault} from '@xh/hoist/utils/js'; import {getLayoutProps} from '@xh/hoist/utils/react'; import {isArray} from 'lodash'; -import {ReactNode} from 'react'; +import {ForwardedRef, ReactNode} from 'react'; import './Slider.scss'; export interface SliderProps extends HoistProps, HoistInputProps, LayoutProps { @@ -111,6 +111,6 @@ const cmp = hoistCmp.factory(({model, className, ...props}, re onBlur: model.onBlur, onFocus: model.onFocus, - ref + ref: ref as ForwardedRef }); }); diff --git a/desktop/cmp/panel/impl/ResizeContainer.ts b/desktop/cmp/panel/impl/ResizeContainer.ts index dc4f37eb04..dd945e00d7 100644 --- a/desktop/cmp/panel/impl/ResizeContainer.ts +++ b/desktop/cmp/panel/impl/ResizeContainer.ts @@ -8,16 +8,14 @@ import composeRefs from '@seznam/compose-react-refs'; import {box, hbox, vbox} from '@xh/hoist/cmp/layout'; import {hoistCmp, HoistProps, TestSupportProps, useContextModel} from '@xh/hoist/core'; import {isString} from 'lodash'; -import {Children, ForwardedRef} from 'react'; +import {Children, RefAttributes} from 'react'; import {PanelModel} from '../PanelModel'; import {dragger} from './dragger/Dragger'; import {splitter} from './Splitter'; -interface ResizeContainerProps extends HoistProps, TestSupportProps { - ref?: ForwardedRef; -} - -export const resizeContainer = hoistCmp.factory({ +export const resizeContainer = hoistCmp.factory< + HoistProps & TestSupportProps & RefAttributes +>({ displayName: 'ResizeContainer', model: false, className: 'xh-resizable', diff --git a/desktop/cmp/treemap/TreeMap.ts b/desktop/cmp/treemap/TreeMap.ts index 20463d32c7..46a4bc90ad 100644 --- a/desktop/cmp/treemap/TreeMap.ts +++ b/desktop/cmp/treemap/TreeMap.ts @@ -34,12 +34,14 @@ import equal from 'fast-deep-equal'; import {assign, cloneDeep, debounce, isFunction, merge, omit} from 'lodash'; import './TreeMap.scss'; -import {ForwardedRef} from 'react'; +import {RefAttributes} from 'react'; import {TreeMapModel} from './TreeMapModel'; -export interface TreeMapProps extends HoistProps, LayoutProps, TestSupportProps { - ref?: ForwardedRef; -} +export interface TreeMapProps + extends HoistProps, + LayoutProps, + TestSupportProps, + RefAttributes {} /** * Component for rendering a TreeMap. diff --git a/mobile/cmp/button/Button.ts b/mobile/cmp/button/Button.ts index 1842c84ce1..aefab651c4 100644 --- a/mobile/cmp/button/Button.ts +++ b/mobile/cmp/button/Button.ts @@ -10,13 +10,14 @@ import {button as onsenButton} from '@xh/hoist/kit/onsen'; import '@xh/hoist/mobile/register'; import {splitLayoutProps} from '@xh/hoist/utils/react'; import classNames from 'classnames'; -import {ReactNode, ReactElement, MouseEvent, ForwardedRef} from 'react'; +import {ReactNode, ReactElement, MouseEvent, RefAttributes} from 'react'; import './Button.scss'; export interface ButtonProps extends HoistProps, LayoutProps, - StyleProps { + StyleProps, + RefAttributes { active?: boolean; disabled?: boolean; icon?: ReactElement; @@ -28,7 +29,6 @@ export interface ButtonProps value?: any; modifier?: string; tabIndex?: number; - ref?: ForwardedRef; } /** diff --git a/mobile/cmp/error/ErrorMessage.ts b/mobile/cmp/error/ErrorMessage.ts index 1db540a42f..5ddc7eedc1 100644 --- a/mobile/cmp/error/ErrorMessage.ts +++ b/mobile/cmp/error/ErrorMessage.ts @@ -9,12 +9,12 @@ import {hoistCmp, HoistProps} from '@xh/hoist/core'; import {button, ButtonProps} from '@xh/hoist/mobile/cmp/button'; import '@xh/hoist/mobile/register'; import {isNil, isString} from 'lodash'; -import {isValidElement, ReactNode, MouseEvent} from 'react'; +import {isValidElement, ReactNode, MouseEvent, RefAttributes} from 'react'; import './ErrorMessage.scss'; import {Icon} from '@xh/hoist/icon'; -export interface ErrorMessageProps extends HoistProps { +export interface ErrorMessageProps extends HoistProps, RefAttributes { /** * If provided, will render a "Retry" button that calls this function. * Use `actionButtonProps` for further control over this button. diff --git a/mobile/cmp/grid/impl/ColChooser.ts b/mobile/cmp/grid/impl/ColChooser.ts index 3a44af2003..b29e40382e 100644 --- a/mobile/cmp/grid/impl/ColChooser.ts +++ b/mobile/cmp/grid/impl/ColChooser.ts @@ -15,6 +15,7 @@ import '@xh/hoist/mobile/register'; import classNames from 'classnames'; import './ColChooser.scss'; import {isEmpty} from 'lodash'; +import {ForwardedRef} from 'react'; import {ColChooserModel} from './ColChooserModel'; export interface ColChooserProps extends HoistProps {} @@ -141,7 +142,7 @@ const columnList = hoistCmp.factory({ ? placeholderCmp('All columns have been added to the grid.') : [...cols.map((col, idx) => draggableRow({col, idx})), placeholder], ...props, - ref + ref: ref as ForwardedRef }); } }); @@ -208,7 +209,7 @@ const row = hoistCmp.factory({ }) ], ...props, - ref + ref: ref as ForwardedRef }); } }); diff --git a/mobile/cmp/header/AppBar.ts b/mobile/cmp/header/AppBar.ts index 2ae2322522..e392fe2ca0 100644 --- a/mobile/cmp/header/AppBar.ts +++ b/mobile/cmp/header/AppBar.ts @@ -16,11 +16,11 @@ import { import {NavigatorModel} from '@xh/hoist/mobile/cmp/navigator'; import {toolbar} from '@xh/hoist/mobile/cmp/toolbar'; import '@xh/hoist/mobile/register'; -import {ReactElement, ReactNode} from 'react'; +import {ReactElement, ReactNode, RefAttributes} from 'react'; import './AppBar.scss'; import {appMenuButton, AppMenuButtonProps} from './AppMenuButton'; -export interface AppBarProps extends HoistProps { +export interface AppBarProps extends HoistProps, RefAttributes { /** App icon to display to the left of the title. */ icon?: ReactElement; diff --git a/mobile/cmp/input/ButtonGroupInput.ts b/mobile/cmp/input/ButtonGroupInput.ts index 69c4a7d201..a7fd47773e 100644 --- a/mobile/cmp/input/ButtonGroupInput.ts +++ b/mobile/cmp/input/ButtonGroupInput.ts @@ -11,7 +11,7 @@ import '@xh/hoist/mobile/register'; import {throwIf, warnIf, withDefault} from '@xh/hoist/utils/js'; import {getLayoutProps, getNonLayoutProps} from '@xh/hoist/utils/react'; import {castArray, isEmpty, without} from 'lodash'; -import {Children, cloneElement, isValidElement, ReactNode} from 'react'; +import {Children, cloneElement, ForwardedRef, isValidElement, ReactNode} from 'react'; import './ButtonGroupInput.scss'; export interface ButtonGroupInputProps @@ -126,6 +126,6 @@ const cmp = hoistCmp.factory(({model, className, ...props ...rest, ...getLayoutProps(props), className, - ref + ref: ref as ForwardedRef }); }); diff --git a/mobile/cmp/input/DateInput.ts b/mobile/cmp/input/DateInput.ts index 3af9e85de5..fcde314fa6 100644 --- a/mobile/cmp/input/DateInput.ts +++ b/mobile/cmp/input/DateInput.ts @@ -17,7 +17,7 @@ import {withDefault} from '@xh/hoist/utils/js'; import {getLayoutProps} from '@xh/hoist/utils/react'; import moment from 'moment'; import './DateInput.scss'; -import {ReactElement} from 'react'; +import {ForwardedRef, ReactElement} from 'react'; export interface DateInputProps extends HoistProps, HoistInputProps, StyleProps, LayoutProps { value?: Date | LocalDate; @@ -217,6 +217,6 @@ const cmp = hoistCmp.factory(({model, className, ...props}, ref) ...layoutProps, textAlign }, - ref + ref: ref as ForwardedRef }); }); diff --git a/mobile/cmp/input/Label.ts b/mobile/cmp/input/Label.ts index a94a63bdc0..c1eca15797 100644 --- a/mobile/cmp/input/Label.ts +++ b/mobile/cmp/input/Label.ts @@ -9,6 +9,7 @@ import {div} from '@xh/hoist/cmp/layout'; import {hoistCmp, HoistProps, StyleProps} from '@xh/hoist/core'; import '@xh/hoist/mobile/register'; import './Label.scss'; +import {ForwardedRef} from 'react'; export interface LabelProps extends HoistProps, HoistInputProps, StyleProps {} @@ -35,6 +36,6 @@ const cmp = hoistCmp.factory(({model, className, style, width, children}, ref) = className, style: {...style, whiteSpace: 'nowrap', width}, items: children, - ref + ref: ref as ForwardedRef }); }); diff --git a/mobile/cmp/input/Select.ts b/mobile/cmp/input/Select.ts index ab2cf4a8f0..ccb2bc0d93 100644 --- a/mobile/cmp/input/Select.ts +++ b/mobile/cmp/input/Select.ts @@ -23,7 +23,7 @@ import {throwIf, withDefault} from '@xh/hoist/utils/js'; import {createObservableRef, getLayoutProps} from '@xh/hoist/utils/react'; import debouncePromise from 'debounce-promise'; import {escapeRegExp, isEqual, isNil, isPlainObject, keyBy, merge} from 'lodash'; -import {Children, ReactNode, ReactPortal} from 'react'; +import {Children, ForwardedRef, ReactNode, ReactPortal} from 'react'; import ReactDom from 'react-dom'; import './Select.scss'; @@ -663,7 +663,7 @@ const cmp = hoistCmp.factory(({model, className, ...props}, re item: box({ item: factory(rsProps), className, - ref + ref: ref as ForwardedRef }) }), model.getOrCreateFullscreenPortalDiv() @@ -674,7 +674,7 @@ const cmp = hoistCmp.factory(({model, className, ...props}, re className, ...layoutProps, width: withDefault(width, null), - ref + ref: ref as ForwardedRef }); } }); diff --git a/mobile/cmp/input/TextArea.ts b/mobile/cmp/input/TextArea.ts index c936bd9406..4901576cce 100644 --- a/mobile/cmp/input/TextArea.ts +++ b/mobile/cmp/input/TextArea.ts @@ -11,6 +11,7 @@ import '@xh/hoist/mobile/register'; import {withDefault} from '@xh/hoist/utils/js'; import {getLayoutProps} from '@xh/hoist/utils/react'; import './TextArea.scss'; +import {ForwardedRef} from 'react'; export interface TextAreaProps extends HoistProps, HoistInputProps, StyleProps, LayoutProps { value?: string; @@ -98,6 +99,6 @@ const cmp = hoistCmp.factory(({model, className, ...props}, }, className, - ref + ref: ref as ForwardedRef }); }); diff --git a/mobile/cmp/input/TextInput.ts b/mobile/cmp/input/TextInput.ts index 0d0b0ad9a7..2172f6e942 100644 --- a/mobile/cmp/input/TextInput.ts +++ b/mobile/cmp/input/TextInput.ts @@ -15,6 +15,7 @@ import {withDefault} from '@xh/hoist/utils/js'; import {getLayoutProps} from '@xh/hoist/utils/react'; import {isEmpty} from 'lodash'; import './TextInput.scss'; +import {ForwardedRef} from 'react'; export interface TextInputProps extends HoistProps, HoistInputProps, StyleProps, LayoutProps { value?: string; @@ -114,7 +115,7 @@ const cmp = hoistCmp.factory(({model, className, ...props}, ref) const {width, ...layoutProps} = getLayoutProps(props); return hbox({ - ref, + ref: ref as ForwardedRef, className, style: { ...props.style, diff --git a/mobile/cmp/loadingindicator/LoadingIndicator.ts b/mobile/cmp/loadingindicator/LoadingIndicator.ts index 16930144f7..e013320f7f 100644 --- a/mobile/cmp/loadingindicator/LoadingIndicator.ts +++ b/mobile/cmp/loadingindicator/LoadingIndicator.ts @@ -13,8 +13,9 @@ import {withDefault} from '@xh/hoist/utils/js'; import classNames from 'classnames'; import {truncate} from 'lodash'; import './LoadingIndicator.scss'; +import {RefAttributes} from 'react'; -export interface LoadingIndicatorProps extends HoistProps { +export interface LoadingIndicatorProps extends HoistProps, RefAttributes { /** TaskObserver(s) that should be monitored to determine if the Indicator should be displayed. */ bind?: Some; /** Position of the indicator relative to its containing component. */ diff --git a/mobile/cmp/mask/Mask.ts b/mobile/cmp/mask/Mask.ts index fcee53925d..0a3689d52d 100644 --- a/mobile/cmp/mask/Mask.ts +++ b/mobile/cmp/mask/Mask.ts @@ -9,10 +9,10 @@ import {spinner as spinnerCmp} from '@xh/hoist/cmp/spinner'; import {hoistCmp, HoistModel, HoistProps, Some, TaskObserver, useLocalModel} from '@xh/hoist/core'; import '@xh/hoist/mobile/register'; import {withDefault} from '@xh/hoist/utils/js'; -import {ReactNode, MouseEvent} from 'react'; +import {ReactNode, MouseEvent, RefAttributes} from 'react'; import './Mask.scss'; -export interface MaskProps extends HoistProps { +export interface MaskProps extends HoistProps, RefAttributes { /** Task(s) that should be monitored to determine if the mask should be displayed. */ bind?: Some; /** True to display the mask. */ diff --git a/mobile/cmp/menu/impl/Menu.ts b/mobile/cmp/menu/impl/Menu.ts index 2beb23fadd..99ec7ffb89 100644 --- a/mobile/cmp/menu/impl/Menu.ts +++ b/mobile/cmp/menu/impl/Menu.ts @@ -4,7 +4,7 @@ * * Copyright © 2024 Extremely Heavy Industries Inc. */ -import {div, hspacer, vbox} from '@xh/hoist/cmp/layout'; +import {BoxComponentProps, div, hspacer, vbox} from '@xh/hoist/cmp/layout'; import {hoistCmp, HoistModel, useLocalModel, MenuItem, MenuItemLike} from '@xh/hoist/core'; import {listItem} from '@xh/hoist/kit/onsen'; import {makeObservable, bindable} from '@xh/hoist/mobx'; @@ -24,7 +24,14 @@ import './Menu.scss'; * * @internal */ -export const menu = hoistCmp.factory({ + +interface MenuProps extends Omit { + menuItems: MenuItemLike[]; + onDismiss: () => void; + title: ReactNode; +} + +export const menu = hoistCmp.factory({ displayName: 'Menu', className: 'xh-menu', From 18dee68bcec4d43f006650d9f4b10411a4ffbfcf Mon Sep 17 00:00:00 2001 From: Greg Solomon Date: Fri, 7 Jun 2024 16:09:31 -0400 Subject: [PATCH 11/23] Overall type improvements and cleanup --- CHANGELOG.md | 19 +- admin/tabs/cluster/logs/LogViewerModel.ts | 2 +- .../tabs/cluster/websocket/WebSocketModel.ts | 2 +- .../userData/roles/warning/WarningBanner.ts | 4 +- cmp/ag-grid/AgGrid.ts | 6 +- cmp/badge/Badge.ts | 4 +- cmp/chart/Chart.ts | 6 +- cmp/clock/Clock.ts | 4 +- cmp/dataview/DataView.ts | 6 +- cmp/filter/FilterChooserModel.ts | 3 +- cmp/form/BaseFormFieldProps.ts | 2 +- cmp/form/Form.ts | 6 +- cmp/grid/Grid.ts | 6 +- cmp/grid/helpers/GridCountLabel.ts | 4 +- cmp/input/HoistInputModel.ts | 6 +- cmp/input/HoistInputProps.ts | 6 +- cmp/layout/Box.ts | 4 +- cmp/layout/Frame.ts | 4 +- cmp/layout/Placeholder.ts | 4 +- cmp/layout/Spacer.ts | 6 +- cmp/layout/TileFrame.ts | 6 +- cmp/layout/Viewport.ts | 4 +- cmp/pinpad/PinPad.ts | 7 +- cmp/relativetimestamp/RelativeTimestamp.ts | 9 +- cmp/spinner/Spinner.ts | 4 +- cmp/store/StoreCountLabel.ts | 4 +- cmp/store/StoreFilterField.ts | 5 +- cmp/tab/TabSwitcherProps.ts | 2 +- cmp/websocket/WebSocketIndicator.ts | 4 +- cmp/zoneGrid/ZoneGrid.ts | 6 +- core/AppSpec.ts | 2 +- core/HoistComponent.ts | 55 ++-- core/HoistProps.ts | 42 ++- core/model/HoistModel.ts | 4 +- desktop/cmp/appbar/AppBar.ts | 4 +- desktop/cmp/button/Button.ts | 12 +- desktop/cmp/button/ButtonGroup.ts | 6 +- desktop/cmp/button/RefreshButton.ts | 18 +- desktop/cmp/contextmenu/ContextMenu.ts | 2 +- desktop/cmp/dash/canvas/DashCanvas.ts | 7 +- desktop/cmp/dash/container/DashContainer.ts | 8 +- desktop/cmp/dock/impl/DockContainer.ts | 2 +- desktop/cmp/error/ErrorMessage.ts | 6 +- desktop/cmp/filechooser/FileChooser.ts | 2 +- desktop/cmp/filter/FilterChooser.ts | 17 +- desktop/cmp/form/FormField.ts | 26 +- desktop/cmp/grouping/GroupingChooser.ts | 5 +- desktop/cmp/input/ButtonGroupInput.ts | 6 +- desktop/cmp/input/Checkbox.ts | 4 +- desktop/cmp/input/CodeInput.ts | 89 ++++--- desktop/cmp/input/DateInput.ts | 246 +++++++++--------- desktop/cmp/input/NumberInput.ts | 4 +- desktop/cmp/input/RadioInput.ts | 4 +- desktop/cmp/input/Select.ts | 3 +- desktop/cmp/input/Slider.ts | 4 +- desktop/cmp/input/SwitchInput.ts | 4 +- desktop/cmp/input/TextArea.ts | 9 +- desktop/cmp/input/TextInput.ts | 108 ++++---- .../cmp/leftrightchooser/LeftRightChooser.ts | 4 +- desktop/cmp/panel/Panel.ts | 6 +- desktop/cmp/panel/impl/PanelHeader.ts | 4 +- desktop/cmp/panel/impl/ResizeContainer.ts | 8 +- desktop/cmp/panel/impl/Splitter.ts | 4 +- desktop/cmp/rest/RestGrid.ts | 7 +- desktop/cmp/rest/impl/RestFormModel.ts | 2 +- desktop/cmp/toolbar/Toolbar.ts | 9 +- desktop/cmp/toolbar/ToolbarSep.ts | 4 +- desktop/cmp/treemap/SplitTreeMap.ts | 4 +- desktop/cmp/treemap/TreeMap.ts | 6 +- icon/Icon.ts | 6 +- icon/XHLogo.tsx | 4 +- icon/impl/IconCmp.ts | 4 +- kit/blueprint/Dialog.ts | 8 +- mobile/cmp/button/Button.ts | 10 +- mobile/cmp/button/ButtonGroup.ts | 4 +- mobile/cmp/button/RefreshButton.ts | 22 +- mobile/cmp/dialog/Dialog.ts | 2 +- mobile/cmp/error/ErrorMessage.ts | 6 +- mobile/cmp/form/FormField.ts | 33 ++- mobile/cmp/grid/impl/ColChooser.ts | 12 +- mobile/cmp/grouping/GroupingChooser.ts | 6 +- mobile/cmp/header/AppBar.ts | 6 +- mobile/cmp/input/ButtonGroupInput.ts | 7 +- mobile/cmp/input/Checkbox.ts | 4 +- mobile/cmp/input/CheckboxButton.ts | 9 +- mobile/cmp/input/DateInput.ts | 4 +- mobile/cmp/input/Label.ts | 4 +- mobile/cmp/input/NumberInput.ts | 4 +- mobile/cmp/input/SearchInput.ts | 4 +- mobile/cmp/input/Select.ts | 4 +- mobile/cmp/input/SwitchInput.ts | 4 +- mobile/cmp/input/TextArea.ts | 4 +- mobile/cmp/input/TextInput.ts | 4 +- .../cmp/loadingindicator/LoadingIndicator.ts | 12 +- mobile/cmp/mask/Mask.ts | 13 +- mobile/cmp/panel/DialogPanel.ts | 4 +- mobile/cmp/panel/Panel.ts | 6 +- mobile/cmp/toolbar/Toolbar.ts | 4 +- mobile/cmp/toolbar/ToolbarSeparator.ts | 4 +- 99 files changed, 622 insertions(+), 508 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 331beb97e2..aaf6fda101 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,25 @@ # Changelog -## 65.0-SNAPSHOT - Unreleased +## 65.0.0-SNAPSHOT - unreleased + +### 💥 Breaking Changes (upgrade difficulty: 🟢 TRIVIAL - component prop name change) +* `RefreshButton` component `model` prop renamed to `target`. + +### 🐞 Bug Fixes + +* Fix bug where `model` passed to `RelativeTimestamp` was being ignored. + +### ⚙️ Technical + +* Typescript: Overall type improvements and cleanup. Note: `AppConfigs` with `model: false` will + need to specify a `null` model type in the generic argument to `hoistCmp`, `hoistCmp.factory` or + `hoistCmp.withFacotry` to avoid a type error. + +## 64.0.4 - 2024-06-05 ### ⚙️ Technical -* Typescript: Narrow typing of `ref` prop, and improve `ref` typing in JSX. +* Typescript: Improve `ref` typing in JSX. ## 64.0.3 - 2024-05-31 diff --git a/admin/tabs/cluster/logs/LogViewerModel.ts b/admin/tabs/cluster/logs/LogViewerModel.ts index c833edb1db..a7c56a69be 100644 --- a/admin/tabs/cluster/logs/LogViewerModel.ts +++ b/admin/tabs/cluster/logs/LogViewerModel.ts @@ -23,7 +23,7 @@ import {LogDisplayModel} from './LogDisplayModel'; export class LogViewerModel extends BaseInstanceModel { @observable file: string = null; - viewRef = createRef(); + viewRef = createRef(); @managed logDisplayModel = new LogDisplayModel(this); diff --git a/admin/tabs/cluster/websocket/WebSocketModel.ts b/admin/tabs/cluster/websocket/WebSocketModel.ts index b8786cb889..992182d71f 100644 --- a/admin/tabs/cluster/websocket/WebSocketModel.ts +++ b/admin/tabs/cluster/websocket/WebSocketModel.ts @@ -23,7 +23,7 @@ import {RecordActionSpec} from '@xh/hoist/data'; import {AppModel} from '@xh/hoist/admin/AppModel'; export class WebSocketModel extends BaseInstanceModel { - viewRef = createRef(); + viewRef = createRef(); @observable lastRefresh: number; diff --git a/admin/tabs/userData/roles/warning/WarningBanner.ts b/admin/tabs/userData/roles/warning/WarningBanner.ts index 6f1500ede2..d28ee0a23a 100644 --- a/admin/tabs/userData/roles/warning/WarningBanner.ts +++ b/admin/tabs/userData/roles/warning/WarningBanner.ts @@ -5,12 +5,12 @@ * Copyright © 2024 Extremely Heavy Industries Inc. */ import {div} from '@xh/hoist/cmp/layout'; -import {hoistCmp, HoistProps} from '@xh/hoist/core'; +import {hoistCmp, HoistProps, NoModel} from '@xh/hoist/core'; import {toolbar} from '@xh/hoist/desktop/cmp/toolbar'; import {Icon} from '@xh/hoist/icon'; import './WarningBanner.scss'; -export interface WarningBannerProps extends HoistProps { +export interface WarningBannerProps extends HoistProps { compact?: boolean; message: string; } diff --git a/cmp/ag-grid/AgGrid.ts b/cmp/ag-grid/AgGrid.ts index bbe149b787..6af8ffe85d 100644 --- a/cmp/ag-grid/AgGrid.ts +++ b/cmp/ag-grid/AgGrid.ts @@ -23,15 +23,13 @@ import {splitLayoutProps} from '@xh/hoist/utils/react'; import classNames from 'classnames'; import {isNil} from 'lodash'; import './AgGrid.scss'; -import {RefAttributes} from 'react'; import {AgGridModel} from './AgGridModel'; export interface AgGridProps - extends HoistProps, + extends HoistProps, GridOptions, LayoutProps, - TestSupportProps, - RefAttributes {} + TestSupportProps {} /** * Minimal wrapper for AgGridReact, supporting direct use of the ag-Grid component with limited diff --git a/cmp/badge/Badge.ts b/cmp/badge/Badge.ts index 1617d61753..e8a353c05c 100644 --- a/cmp/badge/Badge.ts +++ b/cmp/badge/Badge.ts @@ -5,14 +5,14 @@ * Copyright © 2024 Extremely Heavy Industries Inc. */ import {div} from '@xh/hoist/cmp/layout'; -import {BoxProps, hoistCmp, HoistProps, Intent} from '@xh/hoist/core'; +import {BoxProps, hoistCmp, HoistPropsWithRef, Intent} from '@xh/hoist/core'; import {TEST_ID} from '@xh/hoist/utils/js'; import {splitLayoutProps} from '@xh/hoist/utils/react'; import classNames from 'classnames'; import {merge} from 'lodash'; import './Badge.scss'; -export interface BadgeProps extends HoistProps, BoxProps { +export interface BadgeProps extends HoistPropsWithRef, BoxProps { /** Sets fontsize to half that of parent element (default false). */ compact?: boolean; diff --git a/cmp/chart/Chart.ts b/cmp/chart/Chart.ts index 92f5e64173..4630020ba9 100644 --- a/cmp/chart/Chart.ts +++ b/cmp/chart/Chart.ts @@ -30,7 +30,6 @@ import { useOnVisibleChange } from '@xh/hoist/utils/react'; import {assign, castArray, cloneDeep, forOwn, isEqual, isPlainObject, merge, omit} from 'lodash'; -import {RefAttributes} from 'react'; import {placeholder} from '../layout'; import './Chart.scss'; import {ChartModel} from './ChartModel'; @@ -43,10 +42,9 @@ installZoomoutGesture(Highcharts); installCopyToClipboard(Highcharts); export interface ChartProps - extends HoistProps, + extends HoistProps, LayoutProps, - TestSupportProps, - RefAttributes { + TestSupportProps { /** * Ratio of width-to-height of displayed chart. If defined and greater than 0, the chart will * respect this ratio within the available space. Otherwise, the chart will stretch on both diff --git a/cmp/clock/Clock.ts b/cmp/clock/Clock.ts index 0ac03f692f..151987882e 100644 --- a/cmp/clock/Clock.ts +++ b/cmp/clock/Clock.ts @@ -9,7 +9,7 @@ import { BoxProps, hoistCmp, HoistModel, - HoistProps, + HoistPropsWithRef, managed, useLocalModel, XH @@ -21,7 +21,7 @@ import {MINUTES, ONE_SECOND} from '@xh/hoist/utils/datetime'; import {isNumber} from 'lodash'; import {getLayoutProps} from '../../utils/react'; -export interface ClockProps extends HoistProps, BoxProps { +export interface ClockProps extends HoistPropsWithRef, BoxProps { /** String to display if the timezone is invalid or an offset cannot be fetched. */ errorString?: string; diff --git a/cmp/dataview/DataView.ts b/cmp/dataview/DataView.ts index b418a25b30..bdf343efda 100644 --- a/cmp/dataview/DataView.ts +++ b/cmp/dataview/DataView.ts @@ -21,14 +21,12 @@ import type {GridOptions} from '@xh/hoist/kit/ag-grid'; import {splitLayoutProps} from '@xh/hoist/utils/react'; import {isFunction, merge} from 'lodash'; import './DataView.scss'; -import {RefAttributes} from 'react'; import {DataViewModel} from './DataViewModel'; export interface DataViewProps - extends HoistProps, + extends HoistProps, LayoutProps, - TestSupportProps, - RefAttributes { + TestSupportProps { /** * Options for ag-Grid's API. * diff --git a/cmp/filter/FilterChooserModel.ts b/cmp/filter/FilterChooserModel.ts index 4c1f811116..cac2b92063 100644 --- a/cmp/filter/FilterChooserModel.ts +++ b/cmp/filter/FilterChooserModel.ts @@ -4,6 +4,7 @@ * * Copyright © 2024 Extremely Heavy Industries Inc. */ +import {HoistInputModel} from '@xh/hoist/cmp/input'; import { HoistModel, managed, @@ -145,7 +146,7 @@ export class FilterChooserModel extends HoistModel { @observable.ref selectValue: string[]; @observable favoritesIsOpen = false; @observable unsupportedFilter = false; - inputRef = createObservableRef(); + inputRef = createObservableRef(); constructor({ fieldSpecs, diff --git a/cmp/form/BaseFormFieldProps.ts b/cmp/form/BaseFormFieldProps.ts index 9cf9cdd55f..44b41cf1de 100644 --- a/cmp/form/BaseFormFieldProps.ts +++ b/cmp/form/BaseFormFieldProps.ts @@ -9,7 +9,7 @@ import {BoxProps, HoistProps} from '@xh/hoist/core'; import {ReactNode} from 'react'; import {FieldModel} from './field/FieldModel'; -export interface BaseFormFieldProps extends HoistProps, BoxProps { +export interface BaseFormFieldProps extends HoistProps, BoxProps { /** * CommitOnChange property for underlying HoistInput (for inputs that support). * Defaulted from containing Form. diff --git a/cmp/form/Form.ts b/cmp/form/Form.ts index e3455e3862..f1b7c94c17 100644 --- a/cmp/form/Form.ts +++ b/cmp/form/Form.ts @@ -5,10 +5,10 @@ * Copyright © 2024 Extremely Heavy Industries Inc. */ import { - DefaultHoistProps, elementFactory, hoistCmp, HoistProps, + PlainObject, TestSupportProps, uses } from '@xh/hoist/core'; @@ -21,7 +21,7 @@ import {FormModel} from './FormModel'; /** @internal */ export interface FormContextType { /** Defaults props to be applied to contained fields. */ - fieldDefaults?: Partial & DefaultHoistProps; + fieldDefaults?: Partial & PlainObject; /** Reference to associated FormModel. */ model?: FormModel; @@ -44,7 +44,7 @@ export interface FormProps extends HoistProps, TestSupportProps { * Defaults for certain props on child/nested FormFields. * @see FormField (note there are both desktop and mobile implementations). */ - fieldDefaults?: Partial & DefaultHoistProps; + fieldDefaults?: Partial & PlainObject; } /** diff --git a/cmp/grid/Grid.ts b/cmp/grid/Grid.ts index f291104712..7137cd0422 100644 --- a/cmp/grid/Grid.ts +++ b/cmp/grid/Grid.ts @@ -46,17 +46,15 @@ import {createObservableRef, getLayoutProps} from '@xh/hoist/utils/react'; import classNames from 'classnames'; import {debounce, isEmpty, isEqual, isNil, max, maxBy, merge} from 'lodash'; import './Grid.scss'; -import {RefAttributes} from 'react'; import {GridModel} from './GridModel'; import {columnGroupHeader} from './impl/ColumnGroupHeader'; import {columnHeader} from './impl/ColumnHeader'; import {RowKeyNavSupport} from './impl/RowKeyNavSupport'; export interface GridProps - extends HoistProps, + extends HoistProps, LayoutProps, - TestSupportProps, - RefAttributes { + TestSupportProps { /** * Options for ag-Grid's API. * diff --git a/cmp/grid/helpers/GridCountLabel.ts b/cmp/grid/helpers/GridCountLabel.ts index 822fec2aaa..4c7caa24c3 100644 --- a/cmp/grid/helpers/GridCountLabel.ts +++ b/cmp/grid/helpers/GridCountLabel.ts @@ -5,12 +5,12 @@ * Copyright © 2024 Extremely Heavy Industries Inc. */ import {box} from '@xh/hoist/cmp/layout'; -import {BoxProps, hoistCmp, HoistProps, useContextModel} from '@xh/hoist/core'; +import {BoxProps, hoistCmp, HoistPropsWithRef, useContextModel} from '@xh/hoist/core'; import {fmtNumber} from '@xh/hoist/format'; import {logError, pluralize, singularize, withDefault} from '@xh/hoist/utils/js'; import {GridModel} from '../GridModel'; -export interface GridCountLabelProps extends HoistProps, BoxProps { +export interface GridCountLabelProps extends HoistPropsWithRef, BoxProps { /** GridModel to which this component should bind. */ gridModel?: GridModel; diff --git a/cmp/input/HoistInputModel.ts b/cmp/input/HoistInputModel.ts index 505d9d3f42..87a12e60ad 100644 --- a/cmp/input/HoistInputModel.ts +++ b/cmp/input/HoistInputModel.ts @@ -5,7 +5,7 @@ * Copyright © 2024 Extremely Heavy Industries Inc. */ import {FieldModel} from '@xh/hoist/cmp/form'; -import {DefaultHoistProps, HoistModel, HoistModelClass, useLocalModel} from '@xh/hoist/core'; +import {HoistModel, HoistModelClass, PlainObject, useLocalModel} from '@xh/hoist/core'; import {action, computed, makeObservable, observable} from '@xh/hoist/mobx'; import {createObservableRef} from '@xh/hoist/utils/react'; import classNames from 'classnames'; @@ -328,8 +328,8 @@ export class HoistInputModel extends HoistModel { */ export function useHoistInputModel( component: any, - props: DefaultHoistProps, - ref: ForwardedRef, + props: PlainObject, + ref: ForwardedRef, modelSpec?: HoistModelClass ): ReactElement { const inputModel = useLocalModel(modelSpec ?? HoistInputModel); diff --git a/cmp/input/HoistInputProps.ts b/cmp/input/HoistInputProps.ts index 49a92e039e..acc553cf07 100644 --- a/cmp/input/HoistInputProps.ts +++ b/cmp/input/HoistInputProps.ts @@ -5,10 +5,10 @@ * Copyright © 2024 Extremely Heavy Industries Inc. */ -import {TestSupportProps} from '@xh/hoist/core'; -import {RefAttributes} from 'react'; +import {HoistInputModel} from '@xh/hoist/cmp/input/HoistInputModel'; +import {HoistModel, HoistProps, TestSupportProps} from '@xh/hoist/core'; -export interface HoistInputProps extends TestSupportProps, RefAttributes { +export interface HoistInputProps extends TestSupportProps, HoistProps { /** * Field or model property name from which this component should read and write its value * in controlled mode. Can be set by parent FormField. diff --git a/cmp/layout/Box.ts b/cmp/layout/Box.ts index fb8ce95034..619ce32baf 100644 --- a/cmp/layout/Box.ts +++ b/cmp/layout/Box.ts @@ -4,13 +4,13 @@ * * Copyright © 2024 Extremely Heavy Industries Inc. */ -import {BoxProps, hoistCmp, HoistProps} from '@xh/hoist/core'; +import {BoxProps, hoistCmp, HoistPropsWithRef} from '@xh/hoist/core'; import {TEST_ID} from '@xh/hoist/utils/js'; import {splitLayoutProps} from '@xh/hoist/utils/react'; import {merge} from 'lodash'; import {div} from './Tags'; -export interface BoxComponentProps extends HoistProps, BoxProps {} +export interface BoxComponentProps extends HoistPropsWithRef, BoxProps {} /** * A Component that supports flexbox-based layout of its contents. diff --git a/cmp/layout/Frame.ts b/cmp/layout/Frame.ts index dddb331dd3..bf8b18bdbb 100644 --- a/cmp/layout/Frame.ts +++ b/cmp/layout/Frame.ts @@ -4,10 +4,10 @@ * * Copyright © 2024 Extremely Heavy Industries Inc. */ -import {hoistCmp, BoxProps, HoistProps} from '@xh/hoist/core'; +import {hoistCmp, BoxProps, HoistPropsWithRef} from '@xh/hoist/core'; import {box} from './Box'; -export interface FrameProps extends HoistProps, BoxProps {} +export interface FrameProps extends HoistPropsWithRef, BoxProps {} /** * A Box class that flexes to grow and stretch within its *own* parent via flex:'auto', useful for diff --git a/cmp/layout/Placeholder.ts b/cmp/layout/Placeholder.ts index 6d2e9bf8bd..cc81248bda 100644 --- a/cmp/layout/Placeholder.ts +++ b/cmp/layout/Placeholder.ts @@ -4,11 +4,11 @@ * * Copyright © 2024 Extremely Heavy Industries Inc. */ -import {hoistCmp, BoxProps, setCmpErrorDisplay, HoistProps} from '@xh/hoist/core'; +import {hoistCmp, BoxProps, setCmpErrorDisplay, HoistPropsWithRef} from '@xh/hoist/core'; import {box} from '@xh/hoist/cmp/layout'; import './Placeholder.scss'; -export interface PlaceholderProps extends HoistProps, BoxProps {} +export interface PlaceholderProps extends HoistPropsWithRef, BoxProps {} /** * A thin wrapper around `Box` with standardized, muted styling. diff --git a/cmp/layout/Spacer.ts b/cmp/layout/Spacer.ts index 492793b625..a256b47315 100644 --- a/cmp/layout/Spacer.ts +++ b/cmp/layout/Spacer.ts @@ -4,10 +4,10 @@ * * Copyright © 2024 Extremely Heavy Industries Inc. */ -import {hoistCmp, BoxProps, HoistProps} from '@xh/hoist/core'; +import {hoistCmp, BoxProps, HoistPropsWithRef, HoistProps} from '@xh/hoist/core'; import {box} from './Box'; -export interface SpacerProps extends HoistProps, BoxProps {} +export interface SpacerProps extends HoistPropsWithRef, BoxProps {} /** * A component for inserting a fixed-sized spacer along the main axis of its parent container. @@ -31,7 +31,7 @@ export const [Spacer, spacer] = hoistCmp.withFactory({ /** * A component that stretches to soak up space along the main axis of its parent container. */ -export const [Filler, filler] = hoistCmp.withFactory({ +export const [Filler, filler] = hoistCmp.withFactory>({ displayName: 'Filler', model: false, observer: false, diff --git a/cmp/layout/TileFrame.ts b/cmp/layout/TileFrame.ts index 71bc033d7e..a6bc116588 100644 --- a/cmp/layout/TileFrame.ts +++ b/cmp/layout/TileFrame.ts @@ -4,17 +4,17 @@ * * Copyright © 2024 Extremely Heavy Industries Inc. */ -import {hoistCmp, useLocalModel, HoistModel, BoxProps, HoistProps} from '@xh/hoist/core'; +import {hoistCmp, useLocalModel, HoistModel, BoxProps, HoistPropsWithRef} from '@xh/hoist/core'; import {frame, box} from '@xh/hoist/cmp/layout'; import {useOnResize} from '@xh/hoist/utils/react'; -import {useState, useLayoutEffect, RefAttributes} from 'react'; +import {useState, useLayoutEffect} from 'react'; import {minBy, isEqual} from 'lodash'; import composeRefs from '@seznam/compose-react-refs'; import {Children} from 'react'; import './TileFrame.scss'; -export interface TileFrameProps extends HoistProps, BoxProps, RefAttributes { +export interface TileFrameProps extends HoistPropsWithRef, BoxProps { /** * Desired tile width / height ratio (i.e. desiredRatio: 2 == twice as wide as tall). * The container will strive to meet this ratio, but the final ratio may vary. diff --git a/cmp/layout/Viewport.ts b/cmp/layout/Viewport.ts index e8e98bfa7b..d6fce0adac 100644 --- a/cmp/layout/Viewport.ts +++ b/cmp/layout/Viewport.ts @@ -4,11 +4,11 @@ * * Copyright © 2024 Extremely Heavy Industries Inc. */ -import {hoistCmp, BoxProps, HoistProps} from '@xh/hoist/core'; +import {hoistCmp, BoxProps, HoistPropsWithRef} from '@xh/hoist/core'; import {box} from './Box'; import './Viewport.scss'; -export interface ViewportProps extends HoistProps, BoxProps {} +export interface ViewportProps extends HoistPropsWithRef, BoxProps {} /** * A container for the top level of the application. diff --git a/cmp/pinpad/PinPad.ts b/cmp/pinpad/PinPad.ts index 83de3af7c7..c7b6e2e546 100644 --- a/cmp/pinpad/PinPad.ts +++ b/cmp/pinpad/PinPad.ts @@ -7,14 +7,9 @@ import {hoistCmp, uses, HoistProps, XH, TestSupportProps} from '@xh/hoist/core'; import {pinPadImpl as desktopPinPadImpl} from '@xh/hoist/dynamics/desktop'; import {pinPadImpl as mobilePinPadImpl} from '@xh/hoist/dynamics/mobile'; -import {RefAttributes} from 'react'; - import {PinPadModel} from './PinPadModel'; -export interface PinPadProps - extends HoistProps, - TestSupportProps, - RefAttributes {} +export interface PinPadProps extends HoistProps, TestSupportProps {} /** * Displays a prompt used to get obtain a PIN from the user. * diff --git a/cmp/relativetimestamp/RelativeTimestamp.ts b/cmp/relativetimestamp/RelativeTimestamp.ts index 8aa74fd054..efd0f336a3 100644 --- a/cmp/relativetimestamp/RelativeTimestamp.ts +++ b/cmp/relativetimestamp/RelativeTimestamp.ts @@ -22,7 +22,7 @@ import {Timer} from '@xh/hoist/utils/async'; import {DAYS, HOURS, LocalDate, SECONDS} from '@xh/hoist/utils/datetime'; import {logWarn, withDefault} from '@xh/hoist/utils/js'; -interface RelativeTimestampProps extends HoistProps, BoxProps { +interface RelativeTimestampProps extends HoistProps, BoxProps { /** * Property on context model containing timestamp. * Specify as an alternative to direct `timestamp` prop (and minimize parent re-renders). @@ -91,7 +91,7 @@ export const [RelativeTimestamp, relativeTimestamp] = hoistCmp.withFactory { +export interface SpinnerProps extends HoistProps, ImgHTMLAttributes { /** True to return a smaller 20px image vs default 50px. */ compact?: boolean; } diff --git a/cmp/store/StoreCountLabel.ts b/cmp/store/StoreCountLabel.ts index f5b93b53a3..b289160fc9 100644 --- a/cmp/store/StoreCountLabel.ts +++ b/cmp/store/StoreCountLabel.ts @@ -5,12 +5,12 @@ * Copyright © 2024 Extremely Heavy Industries Inc. */ import {box} from '@xh/hoist/cmp/layout'; -import {BoxProps, hoistCmp, HoistProps} from '@xh/hoist/core'; +import {BoxProps, hoistCmp, HoistPropsWithRef} from '@xh/hoist/core'; import {Store} from '@xh/hoist/data'; import {fmtNumber} from '@xh/hoist/format'; import {pluralize, singularize} from '@xh/hoist/utils/js'; -export interface StoreCountLabelProps extends HoistProps, BoxProps { +export interface StoreCountLabelProps extends HoistPropsWithRef, BoxProps { /** Store to which this component should bind. */ store?: Store; diff --git a/cmp/store/StoreFilterField.ts b/cmp/store/StoreFilterField.ts index 531dfb59aa..165e632d08 100644 --- a/cmp/store/StoreFilterField.ts +++ b/cmp/store/StoreFilterField.ts @@ -11,7 +11,8 @@ import {storeFilterFieldImpl as desktopStoreFilterFieldImpl} from '@xh/hoist/dyn import {storeFilterFieldImpl as mobileStoreFilterFieldImpl} from '@xh/hoist/dynamics/mobile'; import {StoreFilterFieldImplModel} from './impl/StoreFilterFieldImplModel'; -export interface StoreFilterFieldProps extends DefaultHoistProps { +export interface StoreFilterFieldProps + extends DefaultHoistProps { /** * Automatically apply the filter to bound store (default true). Applications that need to * combine the filter generated by this component with other filters or run any other custom @@ -57,7 +58,7 @@ export interface StoreFilterFieldProps extends DefaultHoistProps { matchMode?: 'start' | 'startWord' | 'any'; /** Optional model for raw value binding - see comments on the `bind` prop for details. */ - model?: HoistModel; + model?: M extends null ? never : M; /** * Callback to receive an updated Filter. Typically used in conjunction with `autoApply: false` diff --git a/cmp/tab/TabSwitcherProps.ts b/cmp/tab/TabSwitcherProps.ts index 15fdb61743..93bdf663ce 100644 --- a/cmp/tab/TabSwitcherProps.ts +++ b/cmp/tab/TabSwitcherProps.ts @@ -7,7 +7,7 @@ import {BoxProps, HoistProps, Side} from '@xh/hoist/core'; import {TabContainerModel} from './TabContainerModel'; -export interface TabSwitcherProps extends HoistProps, BoxProps { +export interface TabSwitcherProps extends HoistProps, BoxProps { /** Relative position within the parent TabContainer. Defaults to 'top'. */ orientation?: Side; diff --git a/cmp/websocket/WebSocketIndicator.ts b/cmp/websocket/WebSocketIndicator.ts index 3fcd71ac61..89a55524fa 100644 --- a/cmp/websocket/WebSocketIndicator.ts +++ b/cmp/websocket/WebSocketIndicator.ts @@ -5,11 +5,11 @@ * Copyright © 2024 Extremely Heavy Industries Inc. */ import {hbox, span} from '@xh/hoist/cmp/layout'; -import {BoxProps, hoistCmp, HoistProps, XH} from '@xh/hoist/core'; +import {BoxProps, hoistCmp, HoistProps, NoModel, XH} from '@xh/hoist/core'; import {fmtTime} from '@xh/hoist/format'; import {Icon} from '@xh/hoist/icon'; -export interface WebSocketIndicatorProps extends HoistProps, BoxProps { +export interface WebSocketIndicatorProps extends HoistProps, BoxProps { /** True to display status as an icon only, without text label. */ iconOnly?: boolean; } diff --git a/cmp/zoneGrid/ZoneGrid.ts b/cmp/zoneGrid/ZoneGrid.ts index e23b96ca3d..96875f1976 100644 --- a/cmp/zoneGrid/ZoneGrid.ts +++ b/cmp/zoneGrid/ZoneGrid.ts @@ -11,15 +11,13 @@ import {zoneMapper as desktopZoneMapper} from '@xh/hoist/dynamics/desktop'; import {zoneMapper as mobileZoneMapper} from '@xh/hoist/dynamics/mobile'; import {GridOptions} from '@xh/hoist/kit/ag-grid'; import {splitLayoutProps} from '@xh/hoist/utils/react'; -import {RefAttributes} from 'react'; import {ZoneGridModel} from './ZoneGridModel'; import './ZoneGrid.scss'; export interface ZoneGridProps - extends HoistProps, + extends HoistProps, LayoutProps, - TestSupportProps, - RefAttributes { + TestSupportProps { /** * Options for ag-Grid's API. * diff --git a/core/AppSpec.ts b/core/AppSpec.ts index 27dd0d4138..97a7ace088 100644 --- a/core/AppSpec.ts +++ b/core/AppSpec.ts @@ -40,7 +40,7 @@ export class AppSpec { * Root HoistComponent for the application. Despite the name, * functional components are fully supported and expected. */ - componentClass: ComponentClass | FunctionComponent; + componentClass: ComponentClass> | FunctionComponent>; /** * Container component to be used to host this application. diff --git a/core/HoistComponent.ts b/core/HoistComponent.ts index b94c4c5efb..b831372b80 100644 --- a/core/HoistComponent.ts +++ b/core/HoistComponent.ts @@ -12,7 +12,10 @@ import { DefaultHoistProps, elementFactory, ElementFactory, - TestSupportProps + TestSupportProps, + ModelTypeOf, + RefTypeOf, + PlainObject } from './'; import { useModelLinker, @@ -41,8 +44,7 @@ import { useRef, createElement, FunctionComponent, - useDebugValue, - RefAttributes + useDebugValue } from 'react'; /** @@ -53,20 +55,25 @@ import { * `ref` is passed as the second argument to the render function. */ -export type RenderPropsOf

= Omit; +export type RenderPropsOf

, RefTypeOf

>> = Omit< + P, + 'modelConfig' | 'modelRef' | 'ref' +>; /** * Configuration for creating a Component. May be specified either as a render function, * or an object containing a render function and associated metadata. */ export type ComponentConfig< - P extends HoistProps, - R = P extends RefAttributes ? R : unknown + /** HoistProps used to infer model and ref types */ + P extends HoistProps, RefTypeOf

>, + /** Additional props that may be passed to the render function */ + D extends PlainObject = {} > = - | ((props: RenderPropsOf

, ref?: ForwardedRef) => ReactNode) + | ((props: RenderPropsOf

& D, ref?: ForwardedRef>) => ReactNode) | { /** Render function defining the component. */ - render(props: RenderPropsOf

, ref?: ForwardedRef): ReactNode; + render(props: RenderPropsOf

& D, ref?: ForwardedRef>): ReactNode; /** * Spec defining the model to be rendered by this component. @@ -74,7 +81,7 @@ export type ComponentConfig< * return of {@link uses} or {@link creates} - these factory functions will create a spec for * either externally-provided or internally-created models. Defaults to `uses('*')`. */ - model?: ModelSpec | false; + model?: ModelTypeOf

extends null ? false : ModelSpec>; /** * Base CSS class for this component. Will be combined with any className @@ -132,11 +139,13 @@ let cmpIndex = 0; // index for anonymous component dispay names * - `hoistCmp.withFactory` - return a 2-element list containing both the newly * defined Component and an elementFactory for it. */ -export function hoistCmp( - config: ComponentConfig> +export function hoistCmp( + config: ComponentConfig, PlainObject> // Infer model, but accept all props ): FC>; -export function hoistCmp

(config: ComponentConfig

): FC

; -export function hoistCmp

(config: ComponentConfig

): FC

{ +export function hoistCmp

, RefTypeOf

>>( + config: ComponentConfig

+): FC

; +export function hoistCmp(config) { // 0) Pre-process/parse args. if (isFunction(config)) config = {render: config, displayName: config.name}; @@ -197,10 +206,10 @@ export const hoistComponent = hoistCmp; * * Most typically used by application, this provides a simple element factory. */ -export function hoistCmpFactory( - config: ComponentConfig> +export function hoistCmpFactory( + config: ComponentConfig, PlainObject> // Infer model, but accept all props ): ElementFactory>; -export function hoistCmpFactory

( +export function hoistCmpFactory

, RefTypeOf

>>( config: ComponentConfig

): ElementFactory

; export function hoistCmpFactory(config) { @@ -214,10 +223,10 @@ hoistCmp.factory = hoistCmpFactory; * * Not typically used by applications. */ -export function hoistCmpWithFactory( - config: ComponentConfig> -): [FC>, ElementFactory>]; -export function hoistCmpWithFactory

( +export function hoistCmpWithFactory( + config: ComponentConfig, PlainObject> // Infer model, but accept all props +): [FC>, ElementFactory>]; +export function hoistCmpWithFactory

, RefTypeOf

>>( config: ComponentConfig

): [FC

, ElementFactory

]; export function hoistCmpWithFactory(config) { @@ -348,7 +357,11 @@ function wrapWithModel(render: RenderFn, cfg: Config): RenderFn { //------------------------------------------------------------------------- // Support to resolve/create model at render-time. Used by wrappers above. //------------------------------------------------------------------------- -function useResolvedModel(props: HoistProps, modelLookup: ModelLookup, cfg: Config): ResolvedModel { +function useResolvedModel( + props: HoistProps, + modelLookup: ModelLookup, + cfg: Config +): ResolvedModel { let ref = useRef(null), resolvedModel = ref.current; diff --git a/core/HoistProps.ts b/core/HoistProps.ts index c1c45fbccb..87c68233f6 100644 --- a/core/HoistProps.ts +++ b/core/HoistProps.ts @@ -6,22 +6,23 @@ */ import {HoistModel} from '@xh/hoist/core'; import {Property} from 'csstype'; -import {CSSProperties, HTMLAttributes, ReactNode, Ref, RefAttributes} from 'react'; +import {CSSProperties, HTMLAttributes, LegacyRef, ReactNode, Ref} from 'react'; /** - * Props interface for Hoist Components. + * Props interfaces for Hoist Components. * * This interface brings in additional properties that are added to the props * collection by HoistComponent. */ -export interface HoistProps { +export type HoistPropsWithRef = HoistProps; +export interface HoistProps { /** * Associated HoistModel for this Component. Depending on the component, may be specified as - * an instance of a HoistModel, or a configuration object to create one, or left undefined. + * an instance of a HoistModel or left undefined. * HoistComponent will resolve (i.e. lookup in context or create if needed) a concrete Model * instance and provide it to the Render method of the component. */ - model?: M; + model?: M extends null ? never : M; /** * Used for specifying the *configuration* of a model to be created by Hoist for this component @@ -43,8 +44,34 @@ export interface HoistProps { /** React children. */ children?: ReactNode; + + /** React Ref for this component. */ + ref?: R extends never ? never : LegacyRef; } +/** Alias to be used when a component does not require a model. */ +export type NoModel = null; + +/** Infer the Model type from a HoistProps type. */ +export type ModelTypeOf> = T extends null + ? null + : T extends HoistProps + ? M + : null; + +/** Infer the Ref type from a HoistProps type. */ +export type RefTypeOf> = T extends null + ? never + : T extends HoistProps + ? R + : never; + +/** Extract all non-model and non-ref props from a HoistProps type. */ +export type WithoutModelAndRef> = Omit< + T, + 'model' | 'modelRef' | 'modelConfig' | 'ref' +>; + /** * A version of Hoist props that allows dynamic keys/properties. This is the interface that * Hoist uses for components that do not explicitly specify the type of props they expect. @@ -53,7 +80,7 @@ export interface HoistProps { * props API. */ -export interface DefaultHoistProps extends HoistProps { +export interface DefaultHoistProps extends HoistProps { [x: string]: any; } @@ -66,8 +93,7 @@ export interface DefaultHoistProps extends Ho export interface BoxProps extends LayoutProps, TestSupportProps, - Omit, 'onChange' | 'contextMenu'>, - RefAttributes {} + Omit, 'onChange' | 'contextMenu'> {} /** * Props for Components that accept standard HTML `style` attributes. diff --git a/core/model/HoistModel.ts b/core/model/HoistModel.ts index 8683eba9e7..cbc1392600 100644 --- a/core/model/HoistModel.ts +++ b/core/model/HoistModel.ts @@ -7,7 +7,7 @@ import {action, makeObservable, observable} from '@xh/hoist/mobx'; import {warnIf} from '@xh/hoist/utils/js'; import {forOwn, has, isFunction} from 'lodash'; -import {DefaultHoistProps, HoistBase, managed, PlainObject} from '../'; +import {HoistBase, managed, PlainObject} from '../'; import {instanceManager} from '../impl/InstanceManager'; import {Loadable, LoadSpec, LoadSupport} from '../load'; import {ModelSelector} from './'; @@ -135,7 +135,7 @@ export abstract class HoistModel extends HoistBase implements Loadable { * Observability is based on a shallow computation for each prop (i.e. a reference * change in any particular prop will trigger observers to be notified). */ - get componentProps(): DefaultHoistProps { + get componentProps(): PlainObject { return this._componentProps; } diff --git a/desktop/cmp/appbar/AppBar.ts b/desktop/cmp/appbar/AppBar.ts index 1b2d6f8128..b0c3a8c95f 100644 --- a/desktop/cmp/appbar/AppBar.ts +++ b/desktop/cmp/appbar/AppBar.ts @@ -6,7 +6,7 @@ */ import {span} from '@xh/hoist/cmp/layout'; -import {hoistCmp, HoistProps, HSide, TestSupportProps, XH} from '@xh/hoist/core'; +import {hoistCmp, HoistProps, HSide, NoModel, TestSupportProps, XH} from '@xh/hoist/core'; import {appBarSeparator} from '@xh/hoist/desktop/cmp/appbar'; import {appMenuButton, AppMenuButtonProps, refreshButton} from '@xh/hoist/desktop/cmp/button'; import {whatsNewButton} from '@xh/hoist/desktop/cmp/button/WhatsNewButton'; @@ -17,7 +17,7 @@ import {isEmpty} from 'lodash'; import {ReactElement, ReactNode} from 'react'; import './AppBar.scss'; -export interface AppBarProps extends HoistProps, TestSupportProps { +export interface AppBarProps extends HoistProps, TestSupportProps { /** Position of the AppMenuButton. */ appMenuButtonPosition?: HSide; diff --git a/desktop/cmp/button/Button.ts b/desktop/cmp/button/Button.ts index 916223fbea..f05a65129a 100644 --- a/desktop/cmp/button/Button.ts +++ b/desktop/cmp/button/Button.ts @@ -7,8 +7,7 @@ import {ButtonProps as BpButtonProps} from '@blueprintjs/core'; import { hoistCmp, - HoistModel, - HoistProps, + HoistPropsWithRef, Intent, LayoutProps, StyleProps, @@ -19,16 +18,15 @@ import {button as bpButton} from '@xh/hoist/kit/blueprint'; import {TEST_ID, withDefault} from '@xh/hoist/utils/js'; import {splitLayoutProps} from '@xh/hoist/utils/react'; import classNames from 'classnames'; -import {ReactElement, ReactNode, RefAttributes} from 'react'; +import {ReactElement, ReactNode} from 'react'; import './Button.scss'; -export interface ButtonProps - extends HoistProps, +export interface ButtonProps + extends HoistPropsWithRef, StyleProps, LayoutProps, TestSupportProps, - Omit, - RefAttributes { + Omit { active?: boolean; autoFocus?: boolean; disabled?: boolean; diff --git a/desktop/cmp/button/ButtonGroup.ts b/desktop/cmp/button/ButtonGroup.ts index c898459187..664e2b912b 100644 --- a/desktop/cmp/button/ButtonGroup.ts +++ b/desktop/cmp/button/ButtonGroup.ts @@ -17,16 +17,14 @@ import '@xh/hoist/desktop/register'; import {buttonGroup as bpButtonGroup} from '@xh/hoist/kit/blueprint'; import {TEST_ID} from '@xh/hoist/utils/js'; import {splitLayoutProps} from '@xh/hoist/utils/react'; -import {RefAttributes} from 'react'; import {SetOptional} from 'type-fest'; export interface ButtonGroupProps - extends HoistProps, + extends HoistProps, LayoutProps, StyleProps, TestSupportProps, - SetOptional, 'children'>, - RefAttributes { + SetOptional, 'children'> { /** True to have all buttons fill available width equally. */ fill?: boolean; diff --git a/desktop/cmp/button/RefreshButton.ts b/desktop/cmp/button/RefreshButton.ts index 859bad807e..508a7fd8e4 100644 --- a/desktop/cmp/button/RefreshButton.ts +++ b/desktop/cmp/button/RefreshButton.ts @@ -4,13 +4,24 @@ * * Copyright © 2024 Extremely Heavy Industries Inc. */ -import {hoistCmp, HoistModel, RefreshContextModel, useContextModel} from '@xh/hoist/core'; +import { + hoistCmp, + HoistModel, + WithoutModelAndRef, + RefreshContextModel, + useContextModel, + HoistPropsWithRef +} from '@xh/hoist/core'; import '@xh/hoist/desktop/register'; import {Icon} from '@xh/hoist/icon'; import {errorIf, withDefault} from '@xh/hoist/utils/js'; import {button, ButtonProps} from './Button'; -export type RefreshButtonProps = ButtonProps; +export interface RefreshButtonProps + extends WithoutModelAndRef, + HoistPropsWithRef { + target?: HoistModel; +} /** * Convenience Button preconfigured for use as a trigger for a refresh operation. @@ -23,11 +34,10 @@ export const [RefreshButton, refreshButton] = hoistCmp.withFactory MenuItemLike[]) | ReactElement; -export interface ContextMenuProps extends HoistProps { +export interface ContextMenuProps extends HoistProps { menuItems: MenuItemLike[]; } diff --git a/desktop/cmp/dash/canvas/DashCanvas.ts b/desktop/cmp/dash/canvas/DashCanvas.ts index 49bb849b2c..35cb477936 100644 --- a/desktop/cmp/dash/canvas/DashCanvas.ts +++ b/desktop/cmp/dash/canvas/DashCanvas.ts @@ -21,7 +21,6 @@ import {Classes, overlay} from '@xh/hoist/kit/blueprint'; import {TEST_ID} from '@xh/hoist/utils/js'; import {useOnVisibleChange} from '@xh/hoist/utils/react'; import classNames from 'classnames'; -import {RefAttributes} from 'react'; import ReactGridLayout, {WidthProvider} from 'react-grid-layout'; import {DashCanvasModel} from './DashCanvasModel'; import {dashCanvasContextMenu} from './impl/DashCanvasContextMenu'; @@ -30,9 +29,9 @@ import {dashCanvasView} from './impl/DashCanvasView'; import 'react-grid-layout/css/styles.css'; import './DashCanvas.scss'; -export type DashCanvasProps = HoistProps & - TestSupportProps & - RefAttributes; +export interface DashCanvasProps + extends HoistProps, + TestSupportProps {} /** * Dashboard-style container that allows users to drag-and-drop child widgets into flexible layouts. diff --git a/desktop/cmp/dash/container/DashContainer.ts b/desktop/cmp/dash/container/DashContainer.ts index a0b79f64f7..c468616521 100644 --- a/desktop/cmp/dash/container/DashContainer.ts +++ b/desktop/cmp/dash/container/DashContainer.ts @@ -17,14 +17,14 @@ import { import {mask} from '@xh/hoist/desktop/cmp/mask'; import {Classes, overlay} from '@xh/hoist/kit/blueprint'; import {useOnMount, useOnResize} from '@xh/hoist/utils/react'; -import {RefAttributes, useContext} from 'react'; +import {useContext} from 'react'; import './DashContainer.scss'; import {DashContainerModel} from './DashContainerModel'; import {dashContainerAddViewButton} from './impl/DashContainerContextMenu'; -export type DashContainerProps = HoistProps & - TestSupportProps & - RefAttributes; +export interface DashContainerProps + extends HoistProps, + TestSupportProps {} /** * Display a set of child components in accordance with a DashContainerModel. diff --git a/desktop/cmp/dock/impl/DockContainer.ts b/desktop/cmp/dock/impl/DockContainer.ts index 48648de5bc..68a7fb533f 100644 --- a/desktop/cmp/dock/impl/DockContainer.ts +++ b/desktop/cmp/dock/impl/DockContainer.ts @@ -17,7 +17,7 @@ import {DockContainerProps} from '../DockContainer'; * @internal */ export function dockContainerImpl( - {model, className, compactHeaders, ...props}: DockContainerProps, + {model, modelRef, modelConfig, className, compactHeaders, ...props}: DockContainerProps, ref ) { return hbox({ diff --git a/desktop/cmp/error/ErrorMessage.ts b/desktop/cmp/error/ErrorMessage.ts index f5ad681d96..010d0da272 100644 --- a/desktop/cmp/error/ErrorMessage.ts +++ b/desktop/cmp/error/ErrorMessage.ts @@ -5,7 +5,7 @@ * Copyright © 2024 Extremely Heavy Industries Inc. */ import {div, filler, frame, hbox, p} from '@xh/hoist/cmp/layout'; -import {BoxProps, hoistCmp, HoistProps} from '@xh/hoist/core'; +import {BoxProps, hoistCmp, HoistModel, HoistProps} from '@xh/hoist/core'; import {button, ButtonProps} from '@xh/hoist/desktop/cmp/button'; import '@xh/hoist/desktop/register'; import {isNil, isString} from 'lodash'; @@ -13,7 +13,9 @@ import {isValidElement, ReactNode} from 'react'; import './ErrorMessage.scss'; import {Icon} from '@xh/hoist/icon'; -export interface ErrorMessageProps extends HoistProps, Omit { +export interface ErrorMessageProps + extends HoistProps, + Omit { /** * If provided, will render a "Retry" button that calls this function. * Use `actionButtonProps` for further control over this button. diff --git a/desktop/cmp/filechooser/FileChooser.ts b/desktop/cmp/filechooser/FileChooser.ts index dea19bba61..5b9528cb5a 100644 --- a/desktop/cmp/filechooser/FileChooser.ts +++ b/desktop/cmp/filechooser/FileChooser.ts @@ -14,7 +14,7 @@ import {ReactNode} from 'react'; import './FileChooser.scss'; import {FileChooserModel} from './FileChooserModel'; -export interface FileChooserProps extends HoistProps, BoxProps { +export interface FileChooserProps extends HoistProps, BoxProps { /** File type(s) to accept (e.g. `['.doc', '.docx', '.pdf']`). */ accept?: Some; diff --git a/desktop/cmp/filter/FilterChooser.ts b/desktop/cmp/filter/FilterChooser.ts index 287e317e1f..663ce0f0b8 100644 --- a/desktop/cmp/filter/FilterChooser.ts +++ b/desktop/cmp/filter/FilterChooser.ts @@ -6,7 +6,7 @@ */ import {FilterChooserFilter, FilterChooserModel} from '@xh/hoist/cmp/filter'; import {box, div, hbox, hframe, vbox} from '@xh/hoist/cmp/layout'; -import {hoistCmp, HoistProps, LayoutProps, uses} from '@xh/hoist/core'; +import {hoistCmp, HoistProps, LayoutProps, NoModel, uses} from '@xh/hoist/core'; import {button} from '@xh/hoist/desktop/cmp/button'; import {select} from '@xh/hoist/desktop/cmp/input'; import '@xh/hoist/desktop/register'; @@ -16,13 +16,12 @@ import {withDefault} from '@xh/hoist/utils/js'; import {splitLayoutProps} from '@xh/hoist/utils/react'; import classNames from 'classnames'; import {isEmpty, sortBy} from 'lodash'; -import {ReactElement, RefAttributes} from 'react'; +import {ReactElement} from 'react'; import './FilterChooser.scss'; export interface FilterChooserProps - extends HoistProps, - LayoutProps, - RefAttributes { + extends HoistProps, + LayoutProps { /** True to focus the control on render. */ autoFocus?: boolean; /** True to disable user interaction. */ @@ -130,7 +129,7 @@ function optionRenderer(opt) { return null; } -const fieldOption = hoistCmp.factory({ +const fieldOption = hoistCmp.factory({ model: false, observer: false, memo: false, @@ -148,7 +147,7 @@ const fieldOption = hoistCmp.factory({ } }); -const minimalFieldOption = hoistCmp.factory({ +const minimalFieldOption = hoistCmp.factory({ model: false, observer: false, memo: false, @@ -161,7 +160,7 @@ const minimalFieldOption = hoistCmp.factory({ } }); -const filterOption = hoistCmp.factory({ +const filterOption = hoistCmp.factory({ model: false, observer: false, render({fieldSpec, displayOp, displayValue}) { @@ -176,7 +175,7 @@ const filterOption = hoistCmp.factory({ } }); -const messageOption = hoistCmp.factory({ +const messageOption = hoistCmp.factory({ model: false, observer: false, render({label}) { diff --git a/desktop/cmp/form/FormField.ts b/desktop/cmp/form/FormField.ts index 85741dba88..56d5a41bb8 100644 --- a/desktop/cmp/form/FormField.ts +++ b/desktop/cmp/form/FormField.ts @@ -9,10 +9,11 @@ import composeRefs from '@seznam/compose-react-refs/composeRefs'; import {BaseFormFieldProps, FieldModel, FormContext, FormContextType} from '@xh/hoist/cmp/form'; import {box, div, label as labelEl, li, span, ul} from '@xh/hoist/cmp/layout'; import { - DefaultHoistProps, hoistCmp, HoistProps, HSide, + NoModel, + PlainObject, TestSupportProps, uses, XH @@ -179,12 +180,12 @@ export const [FormField, formField] = hoistCmp.withFactory({ let childEl: ReactElement = !child || readonly ? readonlyChild({ - model, + fieldModel: model, readonlyRenderer, testId: getTestId(testId, 'readonly-display') }) : editableChild({ - model, + fieldModel: model, child, childIsSizeable, childId, @@ -252,28 +253,29 @@ export const [FormField, formField] = hoistCmp.withFactory({ } }); -interface ReadonlyChildProps extends HoistProps, TestSupportProps { +interface ReadonlyChildProps extends HoistProps, TestSupportProps { + fieldModel: FieldModel; readonlyRenderer: (v: any, model: FieldModel) => ReactNode; } const readonlyChild = hoistCmp.factory({ model: false, - render({model, readonlyRenderer, testId}) { - const value = model ? model['value'] : null; + render({fieldModel, readonlyRenderer, testId}) { + const value = fieldModel ? fieldModel['value'] : null; return div({ className: 'xh-form-field-readonly-display', [TEST_ID]: testId, - item: readonlyRenderer(value, model) + item: readonlyRenderer(value, fieldModel) }); } }); -const editableChild = hoistCmp.factory({ +const editableChild = hoistCmp.factory({ model: false, render({ - model, + fieldModel, child, childIsSizeable, childId, @@ -286,12 +288,12 @@ const editableChild = hoistCmp.factory({ const {props} = child; // Overrides -- be sure not to clobber selected properties on child - const overrides: DefaultHoistProps = { - model, + const overrides: PlainObject = { + model: fieldModel, bind: 'value', id: childId, disabled: props.disabled || disabled, - ref: composeRefs(model?.boundInputRef, child.ref), + ref: composeRefs(fieldModel?.boundInputRef, child.ref), testId: props.testId ?? testId }; diff --git a/desktop/cmp/grouping/GroupingChooser.ts b/desktop/cmp/grouping/GroupingChooser.ts index 8dba97778f..0aa309de45 100644 --- a/desktop/cmp/grouping/GroupingChooser.ts +++ b/desktop/cmp/grouping/GroupingChooser.ts @@ -6,7 +6,7 @@ */ import {GroupingChooserModel} from '@xh/hoist/cmp/grouping'; import {box, div, filler, fragment, hbox, vbox} from '@xh/hoist/cmp/layout'; -import {hoistCmp, uses} from '@xh/hoist/core'; +import {hoistCmp, HoistProps, uses, WithoutModelAndRef} from '@xh/hoist/core'; import {button, ButtonProps} from '@xh/hoist/desktop/cmp/button'; import {select} from '@xh/hoist/desktop/cmp/input'; import {panel} from '@xh/hoist/desktop/cmp/panel'; @@ -22,7 +22,8 @@ import './GroupingChooser.scss'; import {RefAttributes} from 'react'; export interface GroupingChooserProps - extends Omit, 'ref'>, + extends WithoutModelAndRef, + HoistProps, RefAttributes { /** Text to represent empty state (i.e. value = null or []) */ emptyText?: string; diff --git a/desktop/cmp/input/ButtonGroupInput.ts b/desktop/cmp/input/ButtonGroupInput.ts index 086c787343..d34828ec26 100644 --- a/desktop/cmp/input/ButtonGroupInput.ts +++ b/desktop/cmp/input/ButtonGroupInput.ts @@ -5,7 +5,7 @@ * Copyright © 2024 Extremely Heavy Industries Inc. */ import {HoistInputModel, HoistInputProps, useHoistInputModel} from '@xh/hoist/cmp/input'; -import {hoistCmp, HoistModel, Intent, XH} from '@xh/hoist/core'; +import {hoistCmp, Intent, WithoutModelAndRef, XH} from '@xh/hoist/core'; import {Button, buttonGroup, ButtonGroupProps} from '@xh/hoist/desktop/cmp/button'; import '@xh/hoist/desktop/register'; import {ButtonProps} from '@xh/hoist/desktop/cmp/button'; @@ -15,7 +15,7 @@ import {castArray, filter, isEmpty, without} from 'lodash'; import {Children, cloneElement, ForwardedRef, isValidElement} from 'react'; export interface ButtonGroupInputProps - extends Omit, 'onChange' | 'ref'>, + extends Omit, 'onChange'>, HoistInputProps { /** * True to allow buttons to be unselected (aka inactivated). Defaults to false. @@ -163,6 +163,6 @@ const cmp = hoistCmp.factory(({model, className, ...props onBlur: model.onBlur, onFocus: model.onFocus, className, - ref: ref as ForwardedRef + ref: ref as ForwardedRef }); }); diff --git a/desktop/cmp/input/Checkbox.ts b/desktop/cmp/input/Checkbox.ts index 179be3c117..d7f780f644 100644 --- a/desktop/cmp/input/Checkbox.ts +++ b/desktop/cmp/input/Checkbox.ts @@ -5,7 +5,7 @@ * Copyright © 2024 Extremely Heavy Industries Inc. */ import {HoistInputModel, HoistInputProps, useHoistInputModel} from '@xh/hoist/cmp/input'; -import {hoistCmp, HoistProps, HSide, StyleProps} from '@xh/hoist/core'; +import {hoistCmp, HSide, StyleProps} from '@xh/hoist/core'; import '@xh/hoist/desktop/register'; import {checkbox as bpCheckbox} from '@xh/hoist/kit/blueprint'; import {TEST_ID, withDefault} from '@xh/hoist/utils/js'; @@ -14,7 +14,7 @@ import {ReactNode} from 'react'; import './Checkbox.scss'; -export interface CheckboxProps extends HoistProps, HoistInputProps, StyleProps { +export interface CheckboxProps extends HoistInputProps, StyleProps { value?: boolean; /** True to focus the control on render. */ diff --git a/desktop/cmp/input/CodeInput.ts b/desktop/cmp/input/CodeInput.ts index 9d4650e72d..6fe9f75cc3 100644 --- a/desktop/cmp/input/CodeInput.ts +++ b/desktop/cmp/input/CodeInput.ts @@ -6,7 +6,15 @@ */ import {HoistInputModel, HoistInputProps, useHoistInputModel} from '@xh/hoist/cmp/input'; import {box, div, filler, fragment, frame, hbox, label, span, vbox} from '@xh/hoist/cmp/layout'; -import {hoistCmp, HoistProps, LayoutProps, managed, PlainObject, XH} from '@xh/hoist/core'; +import { + BoxProps, + hoistCmp, + HoistProps, + LayoutProps, + managed, + PlainObject, + XH +} from '@xh/hoist/core'; import {button} from '@xh/hoist/desktop/cmp/button'; import {clipboardButton} from '@xh/hoist/desktop/cmp/clipboard'; import {textInput} from '@xh/hoist/desktop/cmp/input/TextInput'; @@ -35,11 +43,11 @@ import 'codemirror/addon/selection/mark-selection.js'; import 'codemirror/lib/codemirror.css'; import 'codemirror/theme/dracula.css'; import {compact, defaultsDeep, isEqual, isFunction} from 'lodash'; -import {ForwardedRef, ReactElement} from 'react'; +import {ReactElement} from 'react'; import {findDOMNode} from 'react-dom'; import './CodeInput.scss'; -export interface CodeInputProps extends HoistProps, HoistInputProps, LayoutProps { +export interface CodeInputProps extends HoistInputProps, LayoutProps { /** True to focus the control on render. */ autoFocus?: boolean; @@ -446,44 +454,47 @@ class CodeInputModel extends HoistInputModel { } } -const cmp = hoistCmp.factory(({model, className, ...props}, ref) => { - return box({ - className: 'xh-code-input__outer-wrapper', - width: 300, - height: 100, - ...getLayoutProps(props), - item: modalSupport({ - model: model.modalSupportModel, - item: inputCmp({ - testId: props.testId, - width: '100%', - height: '100%', - className, - ref, - model +const cmp = hoistCmp.factory & BoxProps>( + ({model, className, ...props}, ref) => { + return box({ + className: 'xh-code-input__outer-wrapper', + width: 300, + height: 100, + ...getLayoutProps(props), + item: modalSupport({ + model: model.modalSupportModel, + item: inputCmp({ + testId: props.testId, + width: '100%', + height: '100%', + className, + ref, + model + }) }) - }) - }); -}); + }); + } +); -const inputCmp = hoistCmp.factory(({model, ...props}, ref) => - vbox({ - items: [ - div({ - className: 'xh-code-input__inner-wrapper', - item: textArea({ - value: model.renderValue || '', - ref: model.manageCodeEditor, - onChange: model.onChange - }) - }), - model.showToolbar ? toolbarCmp() : actionButtonsCmp() - ], - onBlur: model.onBlur, - onFocus: model.onFocus, - ...props, - ref: ref as ForwardedRef - }) +const inputCmp = hoistCmp.factory & BoxProps>( + ({model, ...props}, ref) => + vbox({ + items: [ + div({ + className: 'xh-code-input__inner-wrapper', + item: textArea({ + value: model.renderValue || '', + ref: model.manageCodeEditor, + onChange: model.onChange + }) + }), + model.showToolbar ? toolbarCmp() : actionButtonsCmp() + ], + onBlur: model.onBlur, + onFocus: model.onFocus, + ...props, + ref + }) ); const toolbarCmp = hoistCmp.factory(({model}) => { diff --git a/desktop/cmp/input/DateInput.ts b/desktop/cmp/input/DateInput.ts index 11e4ba3482..ab8f399103 100644 --- a/desktop/cmp/input/DateInput.ts +++ b/desktop/cmp/input/DateInput.ts @@ -8,7 +8,7 @@ import {PopperBoundary, PopperModifierOverrides} from '@blueprintjs/core'; import {TimePickerProps} from '@blueprintjs/datetime'; import {HoistInputModel, HoistInputProps, useHoistInputModel} from '@xh/hoist/cmp/input'; import {div} from '@xh/hoist/cmp/layout'; -import {hoistCmp, HoistProps, HSide, LayoutProps, Some} from '@xh/hoist/core'; +import {WithoutModelAndRef, hoistCmp, HoistProps, HSide, LayoutProps, Some} from '@xh/hoist/core'; import {button, buttonGroup} from '@xh/hoist/desktop/cmp/button'; import {textInput, TextInputModel} from '@xh/hoist/desktop/cmp/input'; import '@xh/hoist/desktop/register'; @@ -27,7 +27,7 @@ import {createRef, ReactElement, ReactNode} from 'react'; import {DayPickerProps} from 'react-day-picker'; import './DateInput.scss'; -export interface DateInputProps extends HoistProps, LayoutProps, HoistInputProps { +export interface DateInputProps extends LayoutProps, HoistInputProps { value?: Date | LocalDate; /** Props passed to ReactDayPicker component, as per DayPicker docs. */ @@ -373,128 +373,128 @@ class DateInputModel extends HoistInputModel { } } -const cmp = hoistCmp.factory( - ({model, className, ...props}, ref) => { - warnIf( - (props.enableClear || props.enablePicker) && props.rightElement, - 'Cannot specify enableClear or enablePicker along with custom rightElement - built-in clear/picker button will not be shown.' - ); - - const enablePicker = props.enablePicker ?? true, - enableTextInput = props.enableTextInput ?? true, - enableClear = props.enableClear ?? false, - disabled = props.disabled ?? false, - isClearable = model.internalValue !== null, - isOpen = enablePicker && model.popoverOpen && !disabled; - - const buttons = buttonGroup({ - padding: 0, - items: [ - button({ - className: 'xh-date-input__clear-icon', - omit: !enableClear || !isClearable || disabled, - icon: Icon.cross(), - tabIndex: -1, - onClick: model.onClearBtnClick, - testId: getTestId(props, 'clear') - }), - button({ - className: classNames( - 'xh-date-input__picker-icon', - enablePicker ? null : 'xh-date-input__picker-icon--disabled' - ), - icon: Icon.calendar(), - tabIndex: enableTextInput || disabled ? -1 : undefined, - ref: model.buttonRef, - onClick: enablePicker && !disabled ? model.onOpenPopoverClick : null, - testId: getTestId(props, 'picker') - }) - ] - }); - const rightElement = withDefault(props.rightElement, buttons); - - let {minDate, maxDate, initialMonth, renderValue} = model; +const cmp = hoistCmp.factory< + HoistProps & WithoutModelAndRef +>(({model, className, ...props}, ref) => { + warnIf( + (props.enableClear || props.enablePicker) && props.rightElement, + 'Cannot specify enableClear or enablePicker along with custom rightElement - built-in clear/picker button will not be shown.' + ); + + const enablePicker = props.enablePicker ?? true, + enableTextInput = props.enableTextInput ?? true, + enableClear = props.enableClear ?? false, + disabled = props.disabled ?? false, + isClearable = model.internalValue !== null, + isOpen = enablePicker && model.popoverOpen && !disabled; + + const buttons = buttonGroup({ + padding: 0, + items: [ + button({ + className: 'xh-date-input__clear-icon', + omit: !enableClear || !isClearable || disabled, + icon: Icon.cross(), + tabIndex: -1, + onClick: model.onClearBtnClick, + testId: getTestId(props, 'clear') + }), + button({ + className: classNames( + 'xh-date-input__picker-icon', + enablePicker ? null : 'xh-date-input__picker-icon--disabled' + ), + icon: Icon.calendar(), + tabIndex: enableTextInput || disabled ? -1 : undefined, + ref: model.buttonRef, + onClick: enablePicker && !disabled ? model.onOpenPopoverClick : null, + testId: getTestId(props, 'picker') + }) + ] + }); + const rightElement = withDefault(props.rightElement, buttons); + + let {minDate, maxDate, initialMonth, renderValue} = model; + + // If app has set an out-of-range date, we render it -- these bounds govern *manual* entry + // But need to relax constraints on the picker, to prevent BP from breaking badly + if (renderValue) { + if (minDate && renderValue < minDate) minDate = renderValue; + if (maxDate && renderValue > maxDate) maxDate = renderValue; + } - // If app has set an out-of-range date, we render it -- these bounds govern *manual* entry - // But need to relax constraints on the picker, to prevent BP from breaking badly - if (renderValue) { - if (minDate && renderValue < minDate) minDate = renderValue; - if (maxDate && renderValue > maxDate) maxDate = renderValue; - } + // BP chooses annoying mid-point if forced to guess initial month. Use closest bound instead + if (!initialMonth && !renderValue) { + const today = new Date(); + if (minDate && today < minDate) initialMonth = minDate; + if (maxDate && today > maxDate) initialMonth = maxDate; + } - // BP chooses annoying mid-point if forced to guess initial month. Use closest bound instead - if (!initialMonth && !renderValue) { - const today = new Date(); - if (minDate && today < minDate) initialMonth = minDate; - if (maxDate && today > maxDate) initialMonth = maxDate; - } + return div({ + className: 'xh-date-input__wrapper', + item: popover({ + isOpen, + minimal: true, + usePortal: true, + autoFocus: false, + enforceFocus: false, + modifiers: props.popoverModifiers, + position: props.popoverPosition ?? 'auto', + boundary: props.popoverBoundary ?? 'viewport', + portalContainer: props.portalContainer ?? document.body, + popoverRef: model.popoverRef, + onClose: model.onPopoverClose, + onInteraction: nextOpenState => { + if (props.showPickerOnFocus) { + model.popoverOpen = nextOpenState; + } else if (!nextOpenState) { + model.popoverOpen = false; + } + }, + + content: bpDatePicker({ + value: renderValue, + onChange: model.onDatePickerChange, + maxDate, + minDate, + initialMonth, + showActionsBar: props.showActionsBar, + dayPickerProps: assign({fixedWeeks: true}, props.dayPickerProps), + timePrecision: model.timePrecision, + timePickerProps: model.timePrecision + ? assign({selectAllOnFocus: true}, props.timePickerProps) + : undefined + }), - return div({ - className: 'xh-date-input__wrapper', - item: popover({ - isOpen, - minimal: true, - usePortal: true, - autoFocus: false, - enforceFocus: false, - modifiers: props.popoverModifiers, - position: props.popoverPosition ?? 'auto', - boundary: props.popoverBoundary ?? 'viewport', - portalContainer: props.portalContainer ?? document.body, - popoverRef: model.popoverRef, - onClose: model.onPopoverClose, - onInteraction: nextOpenState => { - if (props.showPickerOnFocus) { - model.popoverOpen = nextOpenState; - } else if (!nextOpenState) { - model.popoverOpen = false; - } - }, - - content: bpDatePicker({ - value: renderValue, - onChange: model.onDatePickerChange, - maxDate, - minDate, - initialMonth, - showActionsBar: props.showActionsBar, - dayPickerProps: assign({fixedWeeks: true}, props.dayPickerProps), - timePrecision: model.timePrecision, - timePickerProps: model.timePrecision - ? assign({selectAllOnFocus: true}, props.timePickerProps) - : undefined + item: div({ + item: textInput({ + value: model.formatDate(renderValue) as string, + className: classNames( + className, + !enableTextInput && !disabled ? 'xh-date-input--picker-only' : null + ), + onCommit: model.onInputCommit, + onChange: model.onInputChange, + onKeyDown: model.onInputKeyDown, + rightElement, + disabled: disabled || !enableTextInput, + leftIcon: props.leftIcon, + tabIndex: props.tabIndex, + placeholder: props.placeholder, + textAlign: props.textAlign, + selectOnFocus: props.selectOnFocus, + inputRef: model.inputRef, + ref: model.textInputRef, + testId: getTestId(props), + ...getLayoutProps(props) }), - - item: div({ - item: textInput({ - value: model.formatDate(renderValue) as string, - className: classNames( - className, - !enableTextInput && !disabled ? 'xh-date-input--picker-only' : null - ), - onCommit: model.onInputCommit, - onChange: model.onInputChange, - onKeyDown: model.onInputKeyDown, - rightElement, - disabled: disabled || !enableTextInput, - leftIcon: props.leftIcon, - tabIndex: props.tabIndex, - placeholder: props.placeholder, - textAlign: props.textAlign, - selectOnFocus: props.selectOnFocus, - inputRef: model.inputRef, - ref: model.textInputRef, - testId: getTestId(props), - ...getLayoutProps(props) - }), - className: 'xh-date-input__click-target', - onClick: !enableTextInput && !disabled ? model.onOpenPopoverClick : null - }) - }), - onBlur: model.onBlur, - onFocus: model.onFocus, - onKeyDown: model.onKeyDown, - ref - }); - } -); + className: 'xh-date-input__click-target', + onClick: !enableTextInput && !disabled ? model.onOpenPopoverClick : null + }) + }), + onBlur: model.onBlur, + onFocus: model.onFocus, + onKeyDown: model.onKeyDown, + ref + }); +}); diff --git a/desktop/cmp/input/NumberInput.ts b/desktop/cmp/input/NumberInput.ts index b66103c44e..a2b65fd4ef 100644 --- a/desktop/cmp/input/NumberInput.ts +++ b/desktop/cmp/input/NumberInput.ts @@ -6,7 +6,7 @@ */ import composeRefs from '@seznam/compose-react-refs'; import {HoistInputModel, HoistInputProps, useHoistInputModel} from '@xh/hoist/cmp/input'; -import {hoistCmp, HoistProps, HSide, LayoutProps, StyleProps} from '@xh/hoist/core'; +import {hoistCmp, HSide, LayoutProps, StyleProps} from '@xh/hoist/core'; import '@xh/hoist/desktop/register'; import {fmtNumber, parseNumber} from '@xh/hoist/format'; import {numericInput} from '@xh/hoist/kit/blueprint'; @@ -16,7 +16,7 @@ import {getLayoutProps} from '@xh/hoist/utils/react'; import {isNaN, isNil, isNumber, round} from 'lodash'; import {ReactElement, ReactNode, Ref, useLayoutEffect} from 'react'; -export interface NumberInputProps extends HoistProps, LayoutProps, StyleProps, HoistInputProps { +export interface NumberInputProps extends LayoutProps, StyleProps, HoistInputProps { value?: number; /** True to focus the control on render. */ diff --git a/desktop/cmp/input/RadioInput.ts b/desktop/cmp/input/RadioInput.ts index 49304ac5d5..f872b1261b 100644 --- a/desktop/cmp/input/RadioInput.ts +++ b/desktop/cmp/input/RadioInput.ts @@ -5,7 +5,7 @@ * Copyright © 2024 Extremely Heavy Industries Inc. */ import {HoistInputModel, HoistInputProps, useHoistInputModel} from '@xh/hoist/cmp/input'; -import {hoistCmp, HoistProps, HSide} from '@xh/hoist/core'; +import {hoistCmp, HSide} from '@xh/hoist/core'; import '@xh/hoist/desktop/register'; import {radio, radioGroup} from '@xh/hoist/kit/blueprint'; import {computed, makeObservable} from '@xh/hoist/mobx'; @@ -13,7 +13,7 @@ import {getTestId, TEST_ID, withDefault} from '@xh/hoist/utils/js'; import {filter, isObject} from 'lodash'; import './RadioInput.scss'; -export interface RadioInputProps extends HoistProps, HoistInputProps { +export interface RadioInputProps extends HoistInputProps { /** True to display each radio button inline with each other. */ inline?: boolean; diff --git a/desktop/cmp/input/Select.ts b/desktop/cmp/input/Select.ts index fbba7d466a..8382f7febe 100644 --- a/desktop/cmp/input/Select.ts +++ b/desktop/cmp/input/Select.ts @@ -10,7 +10,6 @@ import { Awaitable, createElement, hoistCmp, - HoistProps, LayoutProps, PlainObject, SelectOption, @@ -48,7 +47,7 @@ import './Select.scss'; export const MENU_PORTAL_ID = 'xh-select-input-portal'; -export interface SelectProps extends HoistProps, HoistInputProps, LayoutProps { +export interface SelectProps extends HoistInputProps, LayoutProps { /** True to focus the control on render. */ autoFocus?: boolean; diff --git a/desktop/cmp/input/Slider.ts b/desktop/cmp/input/Slider.ts index fb3fdceb46..3d127537ba 100644 --- a/desktop/cmp/input/Slider.ts +++ b/desktop/cmp/input/Slider.ts @@ -6,7 +6,7 @@ */ import {HoistInputModel, HoistInputProps, useHoistInputModel} from '@xh/hoist/cmp/input'; import {box} from '@xh/hoist/cmp/layout'; -import {hoistCmp, HoistProps, LayoutProps, Some} from '@xh/hoist/core'; +import {hoistCmp, LayoutProps, Some} from '@xh/hoist/core'; import '@xh/hoist/desktop/register'; import {rangeSlider as bpRangeSlider, slider as bpSlider} from '@xh/hoist/kit/blueprint'; import {throwIf, withDefault} from '@xh/hoist/utils/js'; @@ -15,7 +15,7 @@ import {isArray} from 'lodash'; import {ForwardedRef, ReactNode} from 'react'; import './Slider.scss'; -export interface SliderProps extends HoistProps, HoistInputProps, LayoutProps { +export interface SliderProps extends HoistInputProps, LayoutProps { value?: Some; /** Maximum value */ diff --git a/desktop/cmp/input/SwitchInput.ts b/desktop/cmp/input/SwitchInput.ts index 8bba00a602..66f7eb13f9 100644 --- a/desktop/cmp/input/SwitchInput.ts +++ b/desktop/cmp/input/SwitchInput.ts @@ -5,14 +5,14 @@ * Copyright © 2024 Extremely Heavy Industries Inc. */ import {HoistInputModel, HoistInputProps, useHoistInputModel} from '@xh/hoist/cmp/input'; -import {hoistCmp, HoistProps, HSide, StyleProps} from '@xh/hoist/core'; +import {hoistCmp, HSide, StyleProps} from '@xh/hoist/core'; import '@xh/hoist/desktop/register'; import {switchControl} from '@xh/hoist/kit/blueprint'; import {TEST_ID, withDefault} from '@xh/hoist/utils/js'; import {ReactNode} from 'react'; import './SwitchInput.scss'; -export interface SwitchInputProps extends HoistProps, HoistInputProps, StyleProps { +export interface SwitchInputProps extends HoistInputProps, StyleProps { value?: boolean; /** True if the control should appear as an inline element (defaults to true). */ diff --git a/desktop/cmp/input/TextArea.ts b/desktop/cmp/input/TextArea.ts index 4a14ec8955..e56a4e37fb 100644 --- a/desktop/cmp/input/TextArea.ts +++ b/desktop/cmp/input/TextArea.ts @@ -6,7 +6,7 @@ */ import composeRefs from '@seznam/compose-react-refs'; import {HoistInputModel, HoistInputProps, useHoistInputModel} from '@xh/hoist/cmp/input'; -import {hoistCmp, HoistProps, LayoutProps, StyleProps} from '@xh/hoist/core'; +import {hoistCmp, LayoutProps, PlainObject, StyleProps} from '@xh/hoist/core'; import '@xh/hoist/desktop/register'; import {textArea as bpTextarea} from '@xh/hoist/kit/blueprint'; import {TEST_ID, withDefault} from '@xh/hoist/utils/js'; @@ -14,7 +14,7 @@ import {getLayoutProps} from '@xh/hoist/utils/react'; import {Ref} from 'react'; import './TextArea.scss'; -export interface TextAreaProps extends HoistProps, HoistInputProps, LayoutProps, StyleProps { +export interface TextAreaProps extends HoistInputProps, LayoutProps, StyleProps { value?: string; /** True to focus the control on render. */ @@ -78,8 +78,9 @@ class TextAreaInputModel extends HoistInputModel { }; } -const cmp = hoistCmp.factory(({model, className, ...props}, ref) => { - const {width, height, flex, ...layoutProps} = getLayoutProps(props); +const cmp = hoistCmp.factory(({model, className, ...rest}, ref) => { + const props = rest as PlainObject, + {width, height, flex, ...layoutProps} = getLayoutProps(props); return bpTextarea({ value: model.renderValue || '', diff --git a/desktop/cmp/input/TextInput.ts b/desktop/cmp/input/TextInput.ts index 35d4b17c9e..f127937fde 100644 --- a/desktop/cmp/input/TextInput.ts +++ b/desktop/cmp/input/TextInput.ts @@ -7,7 +7,14 @@ import composeRefs from '@seznam/compose-react-refs'; import {HoistInputModel, HoistInputProps, useHoistInputModel} from '@xh/hoist/cmp/input'; import {div} from '@xh/hoist/cmp/layout'; -import {hoistCmp, HoistProps, HSide, LayoutProps, StyleProps} from '@xh/hoist/core'; +import { + WithoutModelAndRef, + hoistCmp, + HoistProps, + HSide, + LayoutProps, + StyleProps +} from '@xh/hoist/core'; import {button} from '@xh/hoist/desktop/cmp/button'; import '@xh/hoist/desktop/register'; import {Icon} from '@xh/hoist/icon'; @@ -17,7 +24,7 @@ import {getLayoutProps} from '@xh/hoist/utils/react'; import {isEmpty} from 'lodash'; import {FocusEvent, ReactElement, ReactNode, Ref} from 'react'; -export interface TextInputProps extends HoistProps, HoistInputProps, LayoutProps, StyleProps { +export interface TextInputProps extends HoistInputProps, LayoutProps, StyleProps { value?: string; /** @@ -113,57 +120,56 @@ export class TextInputModel extends HoistInputModel { this.noteFocused(); }; } - -const cmp = hoistCmp.factory( - ({model, className, ...props}, ref) => { - const {width, flex, ...layoutProps} = getLayoutProps(props); - - const isClearable = !isEmpty(model.internalValue); - - return div({ - item: inputGroup({ - value: model.renderValue || '', - - autoComplete: withDefault( - props.autoComplete, - props.type === 'password' ? 'new-password' : 'off' - ), - autoFocus: props.autoFocus, - disabled: props.disabled, - inputRef: composeRefs(model.inputRef, props.inputRef), - leftIcon: props.leftIcon, - placeholder: props.placeholder, - rightElement: - props.rightElement || - (props.enableClear && !props.disabled && isClearable ? clearButton() : null), - round: withDefault(props.round, false), - spellCheck: withDefault(props.spellCheck, false), - tabIndex: props.tabIndex, - type: props.type, - - id: props.id, - style: { - ...props.style, - ...layoutProps, - textAlign: withDefault(props.textAlign, 'left') - }, - [TEST_ID]: props.testId, - onChange: model.onChange, - onKeyDown: model.onKeyDown - }), - - className, +const cmp = hoistCmp.factory< + HoistProps & WithoutModelAndRef +>(({model, className, ...props}, ref) => { + const {width, flex, ...layoutProps} = getLayoutProps(props); + + const isClearable = !isEmpty(model.internalValue); + + return div({ + item: inputGroup({ + value: model.renderValue || '', + + autoComplete: withDefault( + props.autoComplete, + props.type === 'password' ? 'new-password' : 'off' + ), + autoFocus: props.autoFocus, + disabled: props.disabled, + inputRef: composeRefs(model.inputRef, props.inputRef), + leftIcon: props.leftIcon, + placeholder: props.placeholder, + rightElement: + props.rightElement || + (props.enableClear && !props.disabled && isClearable ? clearButton() : null), + round: withDefault(props.round, false), + spellCheck: withDefault(props.spellCheck, false), + tabIndex: props.tabIndex, + type: props.type, + + id: props.id, style: { - width: withDefault(width, 200), - flex: withDefault(flex, null) + ...props.style, + ...layoutProps, + textAlign: withDefault(props.textAlign, 'left') }, - - onBlur: model.onBlur, - onFocus: model.onFocus, - ref - }); - } -); + [TEST_ID]: props.testId, + onChange: model.onChange, + onKeyDown: model.onKeyDown + }), + + className, + style: { + width: withDefault(width, 200), + flex: withDefault(flex, null) + }, + + onBlur: model.onBlur, + onFocus: model.onFocus, + ref + }); +}); const clearButton = hoistCmp.factory(({model}) => button({ diff --git a/desktop/cmp/leftrightchooser/LeftRightChooser.ts b/desktop/cmp/leftrightchooser/LeftRightChooser.ts index 7217135592..5c81c86cdc 100644 --- a/desktop/cmp/leftrightchooser/LeftRightChooser.ts +++ b/desktop/cmp/leftrightchooser/LeftRightChooser.ts @@ -14,7 +14,9 @@ import './LeftRightChooser.scss'; import {LeftRightChooserModel} from './LeftRightChooserModel'; import {cloneDeep} from 'lodash'; -export interface LeftRightChooserProps extends HoistProps, BoxProps {} +export interface LeftRightChooserProps + extends HoistProps, + BoxProps {} /** * A component for moving a list of items between two arbitrary groups. By convention, the left diff --git a/desktop/cmp/panel/Panel.ts b/desktop/cmp/panel/Panel.ts index 621b992b64..0592539c4e 100644 --- a/desktop/cmp/panel/Panel.ts +++ b/desktop/cmp/panel/Panel.ts @@ -34,7 +34,9 @@ import {resizeContainer} from './impl/ResizeContainer'; import './Panel.scss'; import {PanelModel} from './PanelModel'; -export interface PanelProps extends HoistProps, Omit { +export interface PanelProps + extends HoistProps, + Omit { /** True to style panel header (if displayed) with reduced padding and font-size. */ compactHeader?: boolean; @@ -201,7 +203,7 @@ export const [Panel, panel] = hoistCmp.withFactory({ } // 3) Prepare core layout with header above core. This is what layout props are trampolined to - let item = vbox({ + let item: ReactElement = vbox({ className: 'xh-panel__content', items: [ panelHeader({ diff --git a/desktop/cmp/panel/impl/PanelHeader.ts b/desktop/cmp/panel/impl/PanelHeader.ts index 1e2eeadbb3..ed020488a8 100644 --- a/desktop/cmp/panel/impl/PanelHeader.ts +++ b/desktop/cmp/panel/impl/PanelHeader.ts @@ -5,7 +5,7 @@ * Copyright © 2024 Extremely Heavy Industries Inc. */ import {box, filler, hbox, span, vbox} from '@xh/hoist/cmp/layout'; -import {hoistCmp, useContextModel} from '@xh/hoist/core'; +import {hoistCmp, NoModel, useContextModel} from '@xh/hoist/core'; import {button, modalToggleButton} from '@xh/hoist/desktop/cmp/button'; import {Icon} from '@xh/hoist/icon'; import {withDefault} from '@xh/hoist/utils/js'; @@ -14,7 +14,7 @@ import {isEmpty, isNil} from 'lodash'; import {PanelModel} from '../PanelModel'; import './PanelHeader.scss'; -export const panelHeader = hoistCmp.factory({ +export const panelHeader = hoistCmp.factory({ displayName: 'PanelHeader', model: false, className: 'xh-panel-header', diff --git a/desktop/cmp/panel/impl/ResizeContainer.ts b/desktop/cmp/panel/impl/ResizeContainer.ts index dd945e00d7..938d1e1665 100644 --- a/desktop/cmp/panel/impl/ResizeContainer.ts +++ b/desktop/cmp/panel/impl/ResizeContainer.ts @@ -6,15 +6,15 @@ */ import composeRefs from '@seznam/compose-react-refs'; import {box, hbox, vbox} from '@xh/hoist/cmp/layout'; -import {hoistCmp, HoistProps, TestSupportProps, useContextModel} from '@xh/hoist/core'; +import {hoistCmp, HoistPropsWithRef, TestSupportProps, useContextModel} from '@xh/hoist/core'; import {isString} from 'lodash'; -import {Children, RefAttributes} from 'react'; +import {Children, ReactElement} from 'react'; import {PanelModel} from '../PanelModel'; import {dragger} from './dragger/Dragger'; import {splitter} from './Splitter'; export const resizeContainer = hoistCmp.factory< - HoistProps & TestSupportProps & RefAttributes + HoistPropsWithRef & TestSupportProps >({ displayName: 'ResizeContainer', model: false, @@ -29,7 +29,7 @@ export const resizeContainer = hoistCmp.factory< sizeIsPct = isString(size) && size.endsWith('%'); const boxSize = sizeIsPct ? `calc(100% - ${dragBarWidth})` : size; - let items = [collapsed ? box(child) : box({item: child, [dim]: boxSize})]; + let items: ReactElement[] = [collapsed ? box(child) : box({item: child, [dim]: boxSize})]; if (showSplitter) { const splitterCmp = splitter(); diff --git a/desktop/cmp/panel/impl/Splitter.ts b/desktop/cmp/panel/impl/Splitter.ts index fc67464244..9ecf0bf2fc 100644 --- a/desktop/cmp/panel/impl/Splitter.ts +++ b/desktop/cmp/panel/impl/Splitter.ts @@ -5,13 +5,13 @@ * Copyright © 2024 Extremely Heavy Industries Inc. */ import {hbox, vbox} from '@xh/hoist/cmp/layout'; -import {hoistCmp, useContextModel} from '@xh/hoist/core'; +import {hoistCmp, NoModel, useContextModel} from '@xh/hoist/core'; import {button} from '@xh/hoist/desktop/cmp/button'; import {Icon} from '@xh/hoist/icon'; import {PanelModel} from '../PanelModel'; import './Splitter.scss'; -export const splitter = hoistCmp.factory({ +export const splitter = hoistCmp.factory({ displayName: 'Splitter', model: false, diff --git a/desktop/cmp/rest/RestGrid.ts b/desktop/cmp/rest/RestGrid.ts index 46b6d145dc..cddb07a90e 100644 --- a/desktop/cmp/rest/RestGrid.ts +++ b/desktop/cmp/rest/RestGrid.ts @@ -7,20 +7,19 @@ import {grid} from '@xh/hoist/cmp/grid'; import {fragment} from '@xh/hoist/cmp/layout'; -import {hoistCmp, HoistProps, PlainObject, Some, uses} from '@xh/hoist/core'; +import {hoistCmp, HoistProps, PlainObject, Some, uses, WithoutModelAndRef} from '@xh/hoist/core'; import {MaskProps} from '@xh/hoist/desktop/cmp/mask'; import {panel, PanelProps} from '@xh/hoist/desktop/cmp/panel'; import '@xh/hoist/desktop/register'; import {getTestId} from '@xh/hoist/utils/js'; import {cloneElement, isValidElement, ReactElement, ReactNode} from 'react'; - import {restForm} from './impl/RestForm'; import {restGridToolbar} from './impl/RestGridToolbar'; import {RestGridModel} from './RestGridModel'; export interface RestGridProps - extends HoistProps, - Omit { + extends HoistProps, + WithoutModelAndRef { /** * This constitutes an 'escape hatch' for applications that need to get to the underlying * ag-Grid API. It should be used with care. Settings made here might be overwritten and/or diff --git a/desktop/cmp/rest/impl/RestFormModel.ts b/desktop/cmp/rest/impl/RestFormModel.ts index 149f1ba73f..72fbbc54c4 100644 --- a/desktop/cmp/rest/impl/RestFormModel.ts +++ b/desktop/cmp/rest/impl/RestFormModel.ts @@ -33,7 +33,7 @@ export class RestFormModel extends HoistModel { @observable types: PlainObject = {}; - dialogRef = createRef(); + dialogRef = createRef(); get unit() { return this.parent.unit; diff --git a/desktop/cmp/toolbar/Toolbar.ts b/desktop/cmp/toolbar/Toolbar.ts index dc610c6abf..a168e796ac 100644 --- a/desktop/cmp/toolbar/Toolbar.ts +++ b/desktop/cmp/toolbar/Toolbar.ts @@ -5,7 +5,7 @@ * Copyright © 2024 Extremely Heavy Industries Inc. */ import {filler, fragment, hbox, vbox} from '@xh/hoist/cmp/layout'; -import {BoxProps, hoistCmp, HoistProps} from '@xh/hoist/core'; +import {BoxProps, hoistCmp, HoistPropsWithRef, NoModel} from '@xh/hoist/core'; import {button} from '@xh/hoist/desktop/cmp/button'; import '@xh/hoist/desktop/register'; import {Icon} from '@xh/hoist/icon'; @@ -18,7 +18,7 @@ import {Children} from 'react'; import './Toolbar.scss'; import {toolbarSeparator} from './ToolbarSep'; -export interface ToolbarProps extends HoistProps, BoxProps { +export interface ToolbarProps extends HoistPropsWithRef, BoxProps { /** Set to true to style toolbar with reduced height and font-size. */ compact?: boolean; @@ -49,6 +49,7 @@ export interface ToolbarProps extends HoistProps, BoxProps { * A toolbar with built-in styling and padding. * In horizontal toolbars, items which overflow can be collapsed into a drop-down menu. */ + export const [Toolbar, toolbar] = hoistCmp.withFactory({ displayName: 'Toolbar', model: false, @@ -100,7 +101,7 @@ export const [Toolbar, toolbar] = hoistCmp.withFactory({ //----------------- // Implementation //-------------- -const overflowBox = hoistCmp.factory({ +const overflowBox = hoistCmp.factory({ model: false, observer: false, memo: false, @@ -115,7 +116,7 @@ const overflowBox = hoistCmp.factory({ } }); -const overflowButton = hoistCmp.factory({ +const overflowButton = hoistCmp.factory({ model: false, observer: false, memo: false, diff --git a/desktop/cmp/toolbar/ToolbarSep.ts b/desktop/cmp/toolbar/ToolbarSep.ts index be71671572..b4516ede17 100644 --- a/desktop/cmp/toolbar/ToolbarSep.ts +++ b/desktop/cmp/toolbar/ToolbarSep.ts @@ -5,14 +5,14 @@ * Copyright © 2024 Extremely Heavy Industries Inc. */ import {span} from '@xh/hoist/cmp/layout'; -import {hoistCmp} from '@xh/hoist/core'; +import {hoistCmp, NoModel} from '@xh/hoist/core'; import '@xh/hoist/desktop/register'; import './Toolbar.scss'; /** * Convenience component to insert a pre-styled separator | between Toolbar items. */ -export const [ToolbarSeparator, toolbarSeparator] = hoistCmp.withFactory({ +export const [ToolbarSeparator, toolbarSeparator] = hoistCmp.withFactory({ displayName: 'ToolbarSeparator', model: false, observer: false, diff --git a/desktop/cmp/treemap/SplitTreeMap.ts b/desktop/cmp/treemap/SplitTreeMap.ts index dbd5bf7eaf..6469cbc363 100644 --- a/desktop/cmp/treemap/SplitTreeMap.ts +++ b/desktop/cmp/treemap/SplitTreeMap.ts @@ -18,7 +18,9 @@ import './SplitTreeMap.scss'; import {SplitTreeMapModel} from './SplitTreeMapModel'; import {treeMap} from './TreeMap'; -export interface SplitTreeMapProps extends HoistProps, BoxProps {} +export interface SplitTreeMapProps + extends HoistProps, + BoxProps {} /** * A component which divides data across two TreeMaps. diff --git a/desktop/cmp/treemap/TreeMap.ts b/desktop/cmp/treemap/TreeMap.ts index 46a4bc90ad..f5bbac61fc 100644 --- a/desktop/cmp/treemap/TreeMap.ts +++ b/desktop/cmp/treemap/TreeMap.ts @@ -34,14 +34,12 @@ import equal from 'fast-deep-equal'; import {assign, cloneDeep, debounce, isFunction, merge, omit} from 'lodash'; import './TreeMap.scss'; -import {RefAttributes} from 'react'; import {TreeMapModel} from './TreeMapModel'; export interface TreeMapProps - extends HoistProps, + extends HoistProps, LayoutProps, - TestSupportProps, - RefAttributes {} + TestSupportProps {} /** * Component for rendering a TreeMap. diff --git a/icon/Icon.ts b/icon/Icon.ts index 1897f7dc67..056c64a049 100644 --- a/icon/Icon.ts +++ b/icon/Icon.ts @@ -12,9 +12,11 @@ import {last, pickBy, split, toLower} from 'lodash'; import {iconCmp} from './impl/IconCmp'; import {enhanceFaClasses, iconHtml} from './impl/IconHtml'; import {ReactElement} from 'react'; -import {HoistProps, Intent, Thunkable} from '@xh/hoist/core'; +import {HoistProps, Intent, Thunkable, WithoutModelAndRef} from '@xh/hoist/core'; -export interface IconProps extends HoistProps, Partial> { +export interface IconProps + extends WithoutModelAndRef, + Partial> { /** Name of the icon in FontAwesome. */ iconName?: string; diff --git a/icon/XHLogo.tsx b/icon/XHLogo.tsx index 22d05141a7..c2e8582ae9 100644 --- a/icon/XHLogo.tsx +++ b/icon/XHLogo.tsx @@ -4,14 +4,14 @@ * * Copyright © 2024 Extremely Heavy Industries Inc. */ -import {hoistCmp, XH} from '@xh/hoist/core'; +import {hoistCmp, NoModel, XH} from '@xh/hoist/core'; import {getLayoutProps} from '@xh/hoist/utils/react'; import React from 'react'; /** * The Extremely Heavy corporate XH logo and word mark, in SVG form. */ -export const xhLogo = hoistCmp.factory({ +export const xhLogo = hoistCmp.factory({ model: false, render(props) { diff --git a/icon/impl/IconCmp.ts b/icon/impl/IconCmp.ts index 16728442ae..63078873ed 100644 --- a/icon/impl/IconCmp.ts +++ b/icon/impl/IconCmp.ts @@ -5,7 +5,7 @@ * Copyright © 2024 Extremely Heavy Industries Inc. */ import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; -import {elementFactory, hoistCmp} from '@xh/hoist/core'; +import {elementFactory, hoistCmp, NoModel} from '@xh/hoist/core'; import {enhanceFaClasses} from './IconHtml'; /** @@ -13,7 +13,7 @@ import {enhanceFaClasses} from './IconHtml'; * Applications should use the factory methods on Icon instead. * @internal */ -export const iconCmp = hoistCmp.factory({ +export const iconCmp = hoistCmp.factory({ displayName: 'Icon', observer: false, model: false, diff --git a/kit/blueprint/Dialog.ts b/kit/blueprint/Dialog.ts index 356229d60e..dc8a54125e 100644 --- a/kit/blueprint/Dialog.ts +++ b/kit/blueprint/Dialog.ts @@ -5,12 +5,12 @@ * Copyright © 2024 Extremely Heavy Industries Inc. */ import {div} from '@xh/hoist/cmp/layout'; -import {hoistCmp} from '@xh/hoist/core'; +import {NoModel, hoistCmp} from '@xh/hoist/core'; /** * Dialog Body for Blueprint, wrapped as a Hoist Component. */ -export const [DialogBody, dialogBody] = hoistCmp.withFactory({ +export const [DialogBody, dialogBody] = hoistCmp.withFactory({ displayName: 'DialogBody', className: 'bp5-dialog-body', observer: false, @@ -25,7 +25,7 @@ export const [DialogBody, dialogBody] = hoistCmp.withFactory({ /** * Dialog Footer for Blueprint, wrapped as Hoist Component. */ -export const [DialogFooter, dialogFooter] = hoistCmp.withFactory({ +export const [DialogFooter, dialogFooter] = hoistCmp.withFactory({ displayName: 'DialogFooter', className: 'bp5-dialog-footer', observer: false, @@ -40,7 +40,7 @@ export const [DialogFooter, dialogFooter] = hoistCmp.withFactory({ /** * Dialog Footer Actions for Blueprint, wrapped as HoistComponent. */ -export const [DialogFooterActions, dialogFooterActions] = hoistCmp.withFactory({ +export const [DialogFooterActions, dialogFooterActions] = hoistCmp.withFactory({ displayName: 'DialogFooterActions', className: 'bp5-dialog-footer-actions', observer: false, diff --git a/mobile/cmp/button/Button.ts b/mobile/cmp/button/Button.ts index aefab651c4..9a341f6b3a 100644 --- a/mobile/cmp/button/Button.ts +++ b/mobile/cmp/button/Button.ts @@ -5,19 +5,15 @@ * Copyright © 2024 Extremely Heavy Industries Inc. */ import {hspacer} from '@xh/hoist/cmp/layout'; -import {LayoutProps, StyleProps, hoistCmp, HoistModel, HoistProps, Intent} from '@xh/hoist/core'; +import {LayoutProps, StyleProps, hoistCmp, Intent, HoistPropsWithRef} from '@xh/hoist/core'; import {button as onsenButton} from '@xh/hoist/kit/onsen'; import '@xh/hoist/mobile/register'; import {splitLayoutProps} from '@xh/hoist/utils/react'; import classNames from 'classnames'; -import {ReactNode, ReactElement, MouseEvent, RefAttributes} from 'react'; +import {ReactNode, ReactElement, MouseEvent} from 'react'; import './Button.scss'; -export interface ButtonProps - extends HoistProps, - LayoutProps, - StyleProps, - RefAttributes { +export interface ButtonProps extends HoistPropsWithRef, LayoutProps, StyleProps { active?: boolean; disabled?: boolean; icon?: ReactElement; diff --git a/mobile/cmp/button/ButtonGroup.ts b/mobile/cmp/button/ButtonGroup.ts index fe0648baf8..1af9951d4d 100644 --- a/mobile/cmp/button/ButtonGroup.ts +++ b/mobile/cmp/button/ButtonGroup.ts @@ -5,13 +5,13 @@ * Copyright © 2024 Extremely Heavy Industries Inc. */ import {hbox} from '@xh/hoist/cmp/layout'; -import {BoxProps, hoistCmp, HoistProps, Intent, XH} from '@xh/hoist/core'; +import {BoxProps, hoistCmp, HoistPropsWithRef, Intent, XH} from '@xh/hoist/core'; import {Button, ButtonProps} from '@xh/hoist/mobile/cmp/button'; import '@xh/hoist/mobile/register'; import {Children, cloneElement, isValidElement} from 'react'; import './ButtonGroup.scss'; -export interface ButtonGroupProps extends HoistProps, BoxProps { +export interface ButtonGroupProps extends HoistPropsWithRef, BoxProps { intent?: Intent; minimal?: boolean; outlined?: boolean; diff --git a/mobile/cmp/button/RefreshButton.ts b/mobile/cmp/button/RefreshButton.ts index a9f66130e2..17e6954abf 100644 --- a/mobile/cmp/button/RefreshButton.ts +++ b/mobile/cmp/button/RefreshButton.ts @@ -4,13 +4,24 @@ * * Copyright © 2024 Extremely Heavy Industries Inc. */ -import {hoistCmp, HoistModel, RefreshContextModel, useContextModel} from '@xh/hoist/core'; +import { + hoistCmp, + HoistModel, + HoistPropsWithRef, + RefreshContextModel, + useContextModel, + WithoutModelAndRef +} from '@xh/hoist/core'; import {Icon} from '@xh/hoist/icon'; import {button, ButtonProps} from '@xh/hoist/mobile/cmp/button'; import '@xh/hoist/mobile/register'; import {errorIf} from '@xh/hoist/utils/js'; -export type RefreshButtonProps = ButtonProps; +export interface RefreshButtonProps + extends HoistPropsWithRef, + WithoutModelAndRef { + target?: HoistModel; +} /** * Convenience Button preconfigured for use as a trigger for a refresh operation. @@ -22,16 +33,17 @@ export type RefreshButtonProps = ButtonProps; export const [RefreshButton, refreshButton] = hoistCmp.withFactory({ displayName: 'RefreshButton', model: false, // For consistency with all other buttons -- the model prop here could be replaced by 'target' + // todo - document as breaking change - render({model, icon = Icon.sync(), onClick, ...props}) { + render({target, icon = Icon.sync(), onClick, ...props}) { const refreshContextModel = useContextModel(RefreshContextModel); if (!onClick) { errorIf( - model && !model.loadSupport, + target && !target.loadSupport, 'Models provided to RefreshButton must enable LoadSupport.' ); - model = model ?? refreshContextModel; + const model = target ?? refreshContextModel; onClick = model ? () => model.refreshAsync() : null; } diff --git a/mobile/cmp/dialog/Dialog.ts b/mobile/cmp/dialog/Dialog.ts index 48bb3fbb40..f1c7ac70bd 100644 --- a/mobile/cmp/dialog/Dialog.ts +++ b/mobile/cmp/dialog/Dialog.ts @@ -11,7 +11,7 @@ import '@xh/hoist/mobile/register'; import {ReactElement, ReactNode} from 'react'; import './Dialog.scss'; -export interface DialogProps extends HoistProps { +export interface DialogProps extends HoistProps { isOpen?: boolean; isCancelable?: boolean; onCancel?: () => void; diff --git a/mobile/cmp/error/ErrorMessage.ts b/mobile/cmp/error/ErrorMessage.ts index 5ddc7eedc1..7b53e24ec0 100644 --- a/mobile/cmp/error/ErrorMessage.ts +++ b/mobile/cmp/error/ErrorMessage.ts @@ -5,16 +5,16 @@ * Copyright © 2024 Extremely Heavy Industries Inc. */ import {div, filler, frame, hbox, p} from '@xh/hoist/cmp/layout'; -import {hoistCmp, HoistProps} from '@xh/hoist/core'; +import {hoistCmp, HoistModel, HoistProps} from '@xh/hoist/core'; import {button, ButtonProps} from '@xh/hoist/mobile/cmp/button'; import '@xh/hoist/mobile/register'; import {isNil, isString} from 'lodash'; -import {isValidElement, ReactNode, MouseEvent, RefAttributes} from 'react'; +import {isValidElement, ReactNode, MouseEvent} from 'react'; import './ErrorMessage.scss'; import {Icon} from '@xh/hoist/icon'; -export interface ErrorMessageProps extends HoistProps, RefAttributes { +export interface ErrorMessageProps extends HoistProps { /** * If provided, will render a "Retry" button that calls this function. * Use `actionButtonProps` for further control over this button. diff --git a/mobile/cmp/form/FormField.ts b/mobile/cmp/form/FormField.ts index ab56fba8e6..40ddda2ff9 100644 --- a/mobile/cmp/form/FormField.ts +++ b/mobile/cmp/form/FormField.ts @@ -7,7 +7,15 @@ import composeRefs from '@seznam/compose-react-refs/composeRefs'; import {FieldModel, FormContext, FormContextType, BaseFormFieldProps} from '@xh/hoist/cmp/form'; import {box, div, span} from '@xh/hoist/cmp/layout'; -import {DefaultHoistProps, hoistCmp, HoistProps, TestSupportProps, uses, XH} from '@xh/hoist/core'; +import { + hoistCmp, + HoistProps, + NoModel, + PlainObject, + TestSupportProps, + uses, + XH +} from '@xh/hoist/core'; import {fmtDate, fmtDateTime, fmtNumber} from '@xh/hoist/format'; import {label as labelCmp} from '@xh/hoist/mobile/cmp/input'; import '@xh/hoist/mobile/register'; @@ -105,9 +113,9 @@ export const [FormField, formField] = hoistCmp.withFactory({ let childEl = readonly || !child - ? readonlyChild({model, readonlyRenderer}) + ? readonlyChild({fieldModel: model, readonlyRenderer}) : editableChild({ - model, + fieldModel: model, child, childIsSizeable, disabled, @@ -156,34 +164,35 @@ export const [FormField, formField] = hoistCmp.withFactory({ } }); -interface ReadonlyChildProps extends HoistProps, TestSupportProps { +interface ReadonlyChildProps extends HoistProps, TestSupportProps { + fieldModel: FieldModel; readonlyRenderer: (v: any, model: FieldModel) => ReactNode; } const readonlyChild = hoistCmp.factory({ model: false, - render({model, readonlyRenderer}) { - const value = model ? model['value'] : null; + render({fieldModel, readonlyRenderer}) { + const value = fieldModel ? fieldModel['value'] : null; return div({ className: 'xh-form-field-readonly-display', - item: readonlyRenderer(value, model) + item: readonlyRenderer(value, fieldModel) }); } }); -const editableChild = hoistCmp.factory({ +const editableChild = hoistCmp.factory({ model: false, - render({model, child, childIsSizeable, disabled, commitOnChange, width, height, flex}) { + render({child, childIsSizeable, disabled, commitOnChange, fieldModel, width, height, flex}) { const {props} = child; // Overrides -- be sure not to clobber selected properties on child - const overrides: DefaultHoistProps = { - model, + const overrides: PlainObject = { + model: fieldModel, bind: 'value', disabled: props.disabled || disabled, - ref: composeRefs(model?.boundInputRef, child.ref) + ref: composeRefs(fieldModel?.boundInputRef, child.ref) }; // If FormField is sized and item doesn't specify its own dimensions, diff --git a/mobile/cmp/grid/impl/ColChooser.ts b/mobile/cmp/grid/impl/ColChooser.ts index b29e40382e..72d50339ee 100644 --- a/mobile/cmp/grid/impl/ColChooser.ts +++ b/mobile/cmp/grid/impl/ColChooser.ts @@ -4,6 +4,7 @@ * * Copyright © 2024 Extremely Heavy Industries Inc. */ +import {Column} from '@xh/hoist/cmp/grid'; import {div, filler, placeholder as placeholderCmp} from '@xh/hoist/cmp/layout'; import {hoistCmp, HoistModel, HoistProps, lookup, useLocalModel, uses} from '@xh/hoist/core'; import {Icon} from '@xh/hoist/icon'; @@ -15,7 +16,7 @@ import '@xh/hoist/mobile/register'; import classNames from 'classnames'; import './ColChooser.scss'; import {isEmpty} from 'lodash'; -import {ForwardedRef} from 'react'; +import {ForwardedRef, ReactNode} from 'react'; import {ColChooserModel} from './ColChooserModel'; export interface ColChooserProps extends HoistProps {} @@ -134,7 +135,12 @@ export const [ColChooser, colChooser] = hoistCmp.withFactory({ //------------------------ // Implementation //------------------------ -const columnList = hoistCmp.factory({ +interface ColumnListProps extends HoistProps { + cols: Column[]; + placeholder: ReactNode; +} + +const columnList = hoistCmp.factory({ render({cols, placeholder, className, ...props}, ref) { return div({ className: classNames('xh-col-chooser__list', className), @@ -142,7 +148,7 @@ const columnList = hoistCmp.factory({ ? placeholderCmp('All columns have been added to the grid.') : [...cols.map((col, idx) => draggableRow({col, idx})), placeholder], ...props, - ref: ref as ForwardedRef + ref: ref }); } }); diff --git a/mobile/cmp/grouping/GroupingChooser.ts b/mobile/cmp/grouping/GroupingChooser.ts index b020a80f62..b8ded8f2d0 100644 --- a/mobile/cmp/grouping/GroupingChooser.ts +++ b/mobile/cmp/grouping/GroupingChooser.ts @@ -6,7 +6,7 @@ */ import {GroupingChooserModel} from '@xh/hoist/cmp/grouping'; import {box, div, filler, hbox, placeholder, span, vbox, vframe} from '@xh/hoist/cmp/layout'; -import {hoistCmp, uses} from '@xh/hoist/core'; +import {hoistCmp, HoistProps, uses, WithoutModelAndRef} from '@xh/hoist/core'; import {Icon} from '@xh/hoist/icon'; import {dragDropContext, draggable, droppable} from '@xh/hoist/kit/react-beautiful-dnd'; import {button, ButtonProps} from '@xh/hoist/mobile/cmp/button'; @@ -19,7 +19,9 @@ import {compact, isEmpty, sortBy} from 'lodash'; import './GroupingChooser.scss'; -export interface GroupingChooserProps extends ButtonProps { +export interface GroupingChooserProps + extends WithoutModelAndRef, + HoistProps { /** Text to represent empty state (i.e. value = null or [])*/ emptyText?: string; /** Title for popover (default "GROUP BY") or null to suppress. */ diff --git a/mobile/cmp/header/AppBar.ts b/mobile/cmp/header/AppBar.ts index e392fe2ca0..91dec15658 100644 --- a/mobile/cmp/header/AppBar.ts +++ b/mobile/cmp/header/AppBar.ts @@ -5,7 +5,7 @@ * Copyright © 2024 Extremely Heavy Industries Inc. */ import {div} from '@xh/hoist/cmp/layout'; -import {hoistCmp, HoistProps, HSide, useContextModel, XH} from '@xh/hoist/core'; +import {hoistCmp, HoistPropsWithRef, HSide, useContextModel, XH} from '@xh/hoist/core'; import { button, navigatorBackButton, @@ -16,11 +16,11 @@ import { import {NavigatorModel} from '@xh/hoist/mobile/cmp/navigator'; import {toolbar} from '@xh/hoist/mobile/cmp/toolbar'; import '@xh/hoist/mobile/register'; -import {ReactElement, ReactNode, RefAttributes} from 'react'; +import {ReactElement, ReactNode} from 'react'; import './AppBar.scss'; import {appMenuButton, AppMenuButtonProps} from './AppMenuButton'; -export interface AppBarProps extends HoistProps, RefAttributes { +export interface AppBarProps extends HoistPropsWithRef { /** App icon to display to the left of the title. */ icon?: ReactElement; diff --git a/mobile/cmp/input/ButtonGroupInput.ts b/mobile/cmp/input/ButtonGroupInput.ts index a7fd47773e..3943dfa70d 100644 --- a/mobile/cmp/input/ButtonGroupInput.ts +++ b/mobile/cmp/input/ButtonGroupInput.ts @@ -5,7 +5,7 @@ * Copyright © 2024 Extremely Heavy Industries Inc. */ import {HoistInputModel, HoistInputProps, useHoistInputModel} from '@xh/hoist/cmp/input'; -import {hoistCmp, XH, HoistProps} from '@xh/hoist/core'; +import {hoistCmp, WithoutModelAndRef, XH} from '@xh/hoist/core'; import {Button, buttonGroup, ButtonGroupProps, ButtonProps} from '@xh/hoist/mobile/cmp/button'; import '@xh/hoist/mobile/register'; import {throwIf, warnIf, withDefault} from '@xh/hoist/utils/js'; @@ -15,9 +15,8 @@ import {Children, cloneElement, ForwardedRef, isValidElement, ReactNode} from 'r import './ButtonGroupInput.scss'; export interface ButtonGroupInputProps - extends HoistProps, - HoistInputProps, - Omit { + extends HoistInputProps, + WithoutModelAndRef { /** * True to allow buttons to be unselected (aka inactivated). Used when enableMulti is false. * Defaults to false. diff --git a/mobile/cmp/input/Checkbox.ts b/mobile/cmp/input/Checkbox.ts index 38a7ac17d1..b26cccf269 100644 --- a/mobile/cmp/input/Checkbox.ts +++ b/mobile/cmp/input/Checkbox.ts @@ -5,12 +5,12 @@ * Copyright © 2024 Extremely Heavy Industries Inc. */ import {HoistInputModel, HoistInputProps, useHoistInputModel} from '@xh/hoist/cmp/input'; -import {hoistCmp, HoistProps} from '@xh/hoist/core'; +import {hoistCmp} from '@xh/hoist/core'; import {checkbox as onsenCheckbox} from '@xh/hoist/kit/onsen'; import '@xh/hoist/mobile/register'; import './Checkbox.scss'; -export interface CheckboxProps extends HoistProps, HoistInputProps { +export interface CheckboxProps extends HoistInputProps { value?: boolean; /** Onsen modifier string */ diff --git a/mobile/cmp/input/CheckboxButton.ts b/mobile/cmp/input/CheckboxButton.ts index 13ef248323..34c2cad16e 100644 --- a/mobile/cmp/input/CheckboxButton.ts +++ b/mobile/cmp/input/CheckboxButton.ts @@ -6,13 +6,13 @@ */ import '@xh/hoist/mobile/register'; import {HoistInputModel, HoistInputProps, useHoistInputModel} from '@xh/hoist/cmp/input'; -import {hoistCmp} from '@xh/hoist/core'; +import {hoistCmp, HoistProps, WithoutModelAndRef} from '@xh/hoist/core'; import {Icon} from '@xh/hoist/icon'; import './CheckboxButton.scss'; import {button, ButtonProps} from '@xh/hoist/mobile/cmp/button'; import {withDefault} from '@xh/hoist/utils/js'; -export interface CheckboxButtonProps extends ButtonProps, HoistInputProps { +export interface CheckboxButtonProps extends WithoutModelAndRef, HoistInputProps { value?: boolean; } @@ -36,7 +36,10 @@ class CheckboxButtonInputModel extends HoistInputModel { //---------------------------------- // Implementation //---------------------------------- -const cmp = hoistCmp.factory(({model, text, ...props}, ref) => { +const cmp = hoistCmp.factory< + HoistProps & + WithoutModelAndRef +>(({model, text, ...props}, ref) => { const checked = !!model.renderValue; return button({ text: withDefault(text, model.getField()?.displayName), diff --git a/mobile/cmp/input/DateInput.ts b/mobile/cmp/input/DateInput.ts index fcde314fa6..d35a565340 100644 --- a/mobile/cmp/input/DateInput.ts +++ b/mobile/cmp/input/DateInput.ts @@ -6,7 +6,7 @@ */ import {HoistInputModel, HoistInputProps, useHoistInputModel} from '@xh/hoist/cmp/input'; import {div} from '@xh/hoist/cmp/layout'; -import {hoistCmp, HoistProps, StyleProps, LayoutProps, HSide, PlainObject} from '@xh/hoist/core'; +import {hoistCmp, StyleProps, LayoutProps, HSide, PlainObject} from '@xh/hoist/core'; import {fmtDate} from '@xh/hoist/format'; import {Icon} from '@xh/hoist/icon'; import {singleDatePicker} from '@xh/hoist/kit/react-dates'; @@ -19,7 +19,7 @@ import moment from 'moment'; import './DateInput.scss'; import {ForwardedRef, ReactElement} from 'react'; -export interface DateInputProps extends HoistProps, HoistInputProps, StyleProps, LayoutProps { +export interface DateInputProps extends HoistInputProps, StyleProps, LayoutProps { value?: Date | LocalDate; /** True to show a "clear" button aligned to the right of the control. Default false. */ diff --git a/mobile/cmp/input/Label.ts b/mobile/cmp/input/Label.ts index c1eca15797..e5331ba729 100644 --- a/mobile/cmp/input/Label.ts +++ b/mobile/cmp/input/Label.ts @@ -6,12 +6,12 @@ */ import {HoistInputModel, HoistInputProps, useHoistInputModel} from '@xh/hoist/cmp/input'; import {div} from '@xh/hoist/cmp/layout'; -import {hoistCmp, HoistProps, StyleProps} from '@xh/hoist/core'; +import {hoistCmp, StyleProps} from '@xh/hoist/core'; import '@xh/hoist/mobile/register'; import './Label.scss'; import {ForwardedRef} from 'react'; -export interface LabelProps extends HoistProps, HoistInputProps, StyleProps {} +export interface LabelProps extends HoistInputProps, StyleProps {} /** * A simple label for a form. diff --git a/mobile/cmp/input/NumberInput.ts b/mobile/cmp/input/NumberInput.ts index 22d0dacc70..6d3ce02f7e 100644 --- a/mobile/cmp/input/NumberInput.ts +++ b/mobile/cmp/input/NumberInput.ts @@ -5,7 +5,7 @@ * Copyright © 2024 Extremely Heavy Industries Inc. */ import {HoistInputModel, HoistInputProps, useHoistInputModel} from '@xh/hoist/cmp/input'; -import {hoistCmp, HoistProps, StyleProps, LayoutProps, HSide} from '@xh/hoist/core'; +import {hoistCmp, StyleProps, LayoutProps, HSide} from '@xh/hoist/core'; import {fmtNumber} from '@xh/hoist/format'; import {input} from '@xh/hoist/kit/onsen'; import '@xh/hoist/mobile/register'; @@ -15,7 +15,7 @@ import {getLayoutProps} from '@xh/hoist/utils/react'; import {isNaN, isNil, isNumber, round} from 'lodash'; import './NumberInput.scss'; -export interface NumberInputProps extends HoistProps, HoistInputProps, StyleProps, LayoutProps { +export interface NumberInputProps extends HoistInputProps, StyleProps, LayoutProps { value?: number; /** True to commit on every change/keystroke, default false. */ diff --git a/mobile/cmp/input/SearchInput.ts b/mobile/cmp/input/SearchInput.ts index b618bc2981..0154fd40a8 100644 --- a/mobile/cmp/input/SearchInput.ts +++ b/mobile/cmp/input/SearchInput.ts @@ -5,14 +5,14 @@ * Copyright © 2024 Extremely Heavy Industries Inc. */ import {HoistInputModel, HoistInputProps, useHoistInputModel} from '@xh/hoist/cmp/input'; -import {hoistCmp, HoistProps, HSide} from '@xh/hoist/core'; +import {hoistCmp, HSide} from '@xh/hoist/core'; import {searchInput as onsenSearchInput} from '@xh/hoist/kit/onsen'; import '@xh/hoist/mobile/register'; import {withDefault} from '@xh/hoist/utils/js'; import {getLayoutProps} from '@xh/hoist/utils/react'; import './SearchInput.scss'; -export interface SearchInputProps extends HoistProps, HoistInputProps { +export interface SearchInputProps extends HoistInputProps { value?: string; /** True to commit on every change/keystroke, default false. */ diff --git a/mobile/cmp/input/Select.ts b/mobile/cmp/input/Select.ts index ccb2bc0d93..7f9e6ccfa9 100644 --- a/mobile/cmp/input/Select.ts +++ b/mobile/cmp/input/Select.ts @@ -6,7 +6,7 @@ */ import {HoistInputModel, HoistInputProps, useHoistInputModel} from '@xh/hoist/cmp/input'; import {box, div, hbox, span} from '@xh/hoist/cmp/layout'; -import {hoistCmp, HoistProps, LayoutProps, PlainObject, SelectOption} from '@xh/hoist/core'; +import {hoistCmp, LayoutProps, PlainObject, SelectOption} from '@xh/hoist/core'; import {Icon} from '@xh/hoist/icon'; import { reactAsyncCreatableSelect, @@ -27,7 +27,7 @@ import {Children, ForwardedRef, ReactNode, ReactPortal} from 'react'; import ReactDom from 'react-dom'; import './Select.scss'; -export interface SelectProps extends HoistProps, HoistInputProps, LayoutProps { +export interface SelectProps extends HoistInputProps, LayoutProps { /** * Function to return a "create a new option" string prompt. Requires `allowCreate` true. * Passed current query input. diff --git a/mobile/cmp/input/SwitchInput.ts b/mobile/cmp/input/SwitchInput.ts index 1b63129ab6..3c4b4687f5 100644 --- a/mobile/cmp/input/SwitchInput.ts +++ b/mobile/cmp/input/SwitchInput.ts @@ -5,12 +5,12 @@ * Copyright © 2024 Extremely Heavy Industries Inc. */ import {HoistInputProps, HoistInputModel, useHoistInputModel} from '@xh/hoist/cmp/input'; -import {hoistCmp, HoistProps, StyleProps} from '@xh/hoist/core'; +import {hoistCmp, StyleProps} from '@xh/hoist/core'; import {switchControl} from '@xh/hoist/kit/onsen'; import '@xh/hoist/mobile/register'; import './SwitchInput.scss'; -export interface SwitchInputProps extends HoistProps, HoistInputProps, StyleProps { +export interface SwitchInputProps extends HoistInputProps, StyleProps { value?: string; /** Onsen modifier string */ diff --git a/mobile/cmp/input/TextArea.ts b/mobile/cmp/input/TextArea.ts index 4901576cce..442ae2eff3 100644 --- a/mobile/cmp/input/TextArea.ts +++ b/mobile/cmp/input/TextArea.ts @@ -6,14 +6,14 @@ */ import {HoistInputModel, HoistInputProps, useHoistInputModel} from '@xh/hoist/cmp/input'; import {div, textarea as textareaTag} from '@xh/hoist/cmp/layout'; -import {hoistCmp, HoistProps, LayoutProps, StyleProps} from '@xh/hoist/core'; +import {hoistCmp, LayoutProps, StyleProps} from '@xh/hoist/core'; import '@xh/hoist/mobile/register'; import {withDefault} from '@xh/hoist/utils/js'; import {getLayoutProps} from '@xh/hoist/utils/react'; import './TextArea.scss'; import {ForwardedRef} from 'react'; -export interface TextAreaProps extends HoistProps, HoistInputProps, StyleProps, LayoutProps { +export interface TextAreaProps extends HoistInputProps, StyleProps, LayoutProps { value?: string; /** True to commit on every change/keystroke, default false. */ diff --git a/mobile/cmp/input/TextInput.ts b/mobile/cmp/input/TextInput.ts index 2172f6e942..9a307994b6 100644 --- a/mobile/cmp/input/TextInput.ts +++ b/mobile/cmp/input/TextInput.ts @@ -6,7 +6,7 @@ */ import {HoistInputModel, HoistInputProps, useHoistInputModel} from '@xh/hoist/cmp/input'; import {hbox} from '@xh/hoist/cmp/layout'; -import {hoistCmp, HoistProps, HSide, LayoutProps, StyleProps} from '@xh/hoist/core'; +import {hoistCmp, HSide, LayoutProps, StyleProps} from '@xh/hoist/core'; import {Icon} from '@xh/hoist/icon'; import {input} from '@xh/hoist/kit/onsen'; import {button} from '@xh/hoist/mobile/cmp/button'; @@ -17,7 +17,7 @@ import {isEmpty} from 'lodash'; import './TextInput.scss'; import {ForwardedRef} from 'react'; -export interface TextInputProps extends HoistProps, HoistInputProps, StyleProps, LayoutProps { +export interface TextInputProps extends HoistInputProps, StyleProps, LayoutProps { value?: string; /** diff --git a/mobile/cmp/loadingindicator/LoadingIndicator.ts b/mobile/cmp/loadingindicator/LoadingIndicator.ts index e013320f7f..d6fb49f640 100644 --- a/mobile/cmp/loadingindicator/LoadingIndicator.ts +++ b/mobile/cmp/loadingindicator/LoadingIndicator.ts @@ -7,15 +7,21 @@ import {hbox} from '@xh/hoist/cmp/layout'; import {div} from '@xh/hoist/cmp/layout/Tags'; import {spinner as spinnerCmp} from '@xh/hoist/cmp/spinner'; -import {hoistCmp, HoistModel, HoistProps, Some, TaskObserver, useLocalModel} from '@xh/hoist/core'; +import { + hoistCmp, + HoistModel, + HoistPropsWithRef, + Some, + TaskObserver, + useLocalModel +} from '@xh/hoist/core'; import '@xh/hoist/mobile/register'; import {withDefault} from '@xh/hoist/utils/js'; import classNames from 'classnames'; import {truncate} from 'lodash'; import './LoadingIndicator.scss'; -import {RefAttributes} from 'react'; -export interface LoadingIndicatorProps extends HoistProps, RefAttributes { +export interface LoadingIndicatorProps extends HoistPropsWithRef { /** TaskObserver(s) that should be monitored to determine if the Indicator should be displayed. */ bind?: Some; /** Position of the indicator relative to its containing component. */ diff --git a/mobile/cmp/mask/Mask.ts b/mobile/cmp/mask/Mask.ts index 0a3689d52d..267aa6b6e0 100644 --- a/mobile/cmp/mask/Mask.ts +++ b/mobile/cmp/mask/Mask.ts @@ -6,13 +6,20 @@ */ import {box, div, vbox, vspacer} from '@xh/hoist/cmp/layout'; import {spinner as spinnerCmp} from '@xh/hoist/cmp/spinner'; -import {hoistCmp, HoistModel, HoistProps, Some, TaskObserver, useLocalModel} from '@xh/hoist/core'; +import { + hoistCmp, + HoistModel, + HoistPropsWithRef, + Some, + TaskObserver, + useLocalModel +} from '@xh/hoist/core'; import '@xh/hoist/mobile/register'; import {withDefault} from '@xh/hoist/utils/js'; -import {ReactNode, MouseEvent, RefAttributes} from 'react'; +import {ReactNode, MouseEvent} from 'react'; import './Mask.scss'; -export interface MaskProps extends HoistProps, RefAttributes { +export interface MaskProps extends HoistPropsWithRef { /** Task(s) that should be monitored to determine if the mask should be displayed. */ bind?: Some; /** True to display the mask. */ diff --git a/mobile/cmp/panel/DialogPanel.ts b/mobile/cmp/panel/DialogPanel.ts index d12718bd81..379ee006d7 100644 --- a/mobile/cmp/panel/DialogPanel.ts +++ b/mobile/cmp/panel/DialogPanel.ts @@ -4,13 +4,13 @@ * * Copyright © 2024 Extremely Heavy Industries Inc. */ -import {hoistCmp} from '@xh/hoist/core'; +import {hoistCmp, HoistProps, WithoutModelAndRef} from '@xh/hoist/core'; import {dialog} from '@xh/hoist/kit/onsen'; import '@xh/hoist/mobile/register'; import './DialogPanel.scss'; import {panel, PanelProps} from './Panel'; -export interface DialogPanelProps extends PanelProps { +export interface DialogPanelProps extends HoistProps, WithoutModelAndRef { /** Is the dialog panel shown. */ isOpen?: boolean; } diff --git a/mobile/cmp/panel/Panel.ts b/mobile/cmp/panel/Panel.ts index f484210e94..063f8093af 100644 --- a/mobile/cmp/panel/Panel.ts +++ b/mobile/cmp/panel/Panel.ts @@ -10,10 +10,10 @@ import { TaskObserver, useContextModel, Some, - HoistProps, ElementFactory, hoistCmp, - HoistModel + HoistModel, + HoistPropsWithRef } from '@xh/hoist/core'; import {loadingIndicator} from '@xh/hoist/mobile/cmp/loadingindicator'; import {mask} from '@xh/hoist/mobile/cmp/mask'; @@ -27,7 +27,7 @@ import {panelHeader} from './impl/PanelHeader'; import './Panel.scss'; import {logWarn} from '@xh/hoist/utils/js'; -export interface PanelProps extends HoistProps, Omit { +export interface PanelProps extends HoistPropsWithRef, Omit { /** A toolbar to be docked at the bottom of the panel. */ bbar?: Some; diff --git a/mobile/cmp/toolbar/Toolbar.ts b/mobile/cmp/toolbar/Toolbar.ts index 7cb388ca93..6ea2d4845e 100644 --- a/mobile/cmp/toolbar/Toolbar.ts +++ b/mobile/cmp/toolbar/Toolbar.ts @@ -5,7 +5,7 @@ * Copyright © 2024 Extremely Heavy Industries Inc. */ import {hbox, vbox} from '@xh/hoist/cmp/layout'; -import {BoxProps, hoistCmp, HoistProps} from '@xh/hoist/core'; +import {BoxProps, hoistCmp, HoistPropsWithRef} from '@xh/hoist/core'; import '@xh/hoist/mobile/register'; import {toolbarSeparator} from '@xh/hoist/mobile/cmp/toolbar'; import {filterConsecutiveToolbarSeparators} from '@xh/hoist/utils/impl'; @@ -13,7 +13,7 @@ import classNames from 'classnames'; import './Toolbar.scss'; import {Children} from 'react'; -export interface ToolbarProps extends HoistProps, BoxProps { +export interface ToolbarProps extends HoistPropsWithRef, BoxProps { /** Set to true to vertically align the items of this toolbar */ vertical?: boolean; } diff --git a/mobile/cmp/toolbar/ToolbarSeparator.ts b/mobile/cmp/toolbar/ToolbarSeparator.ts index bea0195d85..07190a521a 100644 --- a/mobile/cmp/toolbar/ToolbarSeparator.ts +++ b/mobile/cmp/toolbar/ToolbarSeparator.ts @@ -5,14 +5,14 @@ * Copyright © 2024 Extremely Heavy Industries Inc. */ import {span} from '@xh/hoist/cmp/layout'; -import {hoistCmp} from '@xh/hoist/core'; +import {hoistCmp, NoModel} from '@xh/hoist/core'; import '@xh/hoist/mobile/register'; import './Toolbar.scss'; /** * Convenience component to insert a pre-styled separator | between Toolbar items. */ -export const [ToolbarSeparator, toolbarSeparator] = hoistCmp.withFactory({ +export const [ToolbarSeparator, toolbarSeparator] = hoistCmp.withFactory({ displayName: 'ToolbarSeparator', className: 'xh-toolbar__separator', model: false, From b8fe9df19b33264874c50d2d81428250e675a213 Mon Sep 17 00:00:00 2001 From: Greg Solomon Date: Fri, 7 Jun 2024 16:35:07 -0400 Subject: [PATCH 12/23] Fix RefreshButton docs --- desktop/cmp/button/RefreshButton.ts | 4 ++-- mobile/cmp/button/RefreshButton.ts | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/desktop/cmp/button/RefreshButton.ts b/desktop/cmp/button/RefreshButton.ts index 508a7fd8e4..2300cb22c1 100644 --- a/desktop/cmp/button/RefreshButton.ts +++ b/desktop/cmp/button/RefreshButton.ts @@ -27,12 +27,12 @@ export interface RefreshButtonProps * Convenience Button preconfigured for use as a trigger for a refresh operation. * * If an onClick handler is provided it will be used. Otherwise this button will - * be linked to any model in props with LoadSupport enabled, or the contextual + * be linked to any target in props with LoadSupport enabled, or the contextual * See {@link RefreshContextModel}. */ export const [RefreshButton, refreshButton] = hoistCmp.withFactory({ displayName: 'RefreshButton', - model: false, // For consistency with all other buttons -- the model prop here could be replaced by 'target' + model: false, render({target, onClick, ...props}, ref) { const refreshContextModel = useContextModel(RefreshContextModel); diff --git a/mobile/cmp/button/RefreshButton.ts b/mobile/cmp/button/RefreshButton.ts index 17e6954abf..0d98c147cc 100644 --- a/mobile/cmp/button/RefreshButton.ts +++ b/mobile/cmp/button/RefreshButton.ts @@ -26,14 +26,13 @@ export interface RefreshButtonProps /** * Convenience Button preconfigured for use as a trigger for a refresh operation. * - * If a model is provided it will be directly refreshed. Alternatively an onClick handler + * If a target is provided it will be directly refreshed. Alternatively an onClick handler * may be provided. If neither of these props are provided, the contextual RefreshContextModel * for this button will be used. */ export const [RefreshButton, refreshButton] = hoistCmp.withFactory({ displayName: 'RefreshButton', - model: false, // For consistency with all other buttons -- the model prop here could be replaced by 'target' - // todo - document as breaking change + model: false, render({target, icon = Icon.sync(), onClick, ...props}) { const refreshContextModel = useContextModel(RefreshContextModel); From fb91079c19e80c6a503f3430918dd3097760a3ae Mon Sep 17 00:00:00 2001 From: Greg Solomon Date: Fri, 7 Jun 2024 16:44:40 -0400 Subject: [PATCH 13/23] Fix HoistProps docstring --- core/HoistProps.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/HoistProps.ts b/core/HoistProps.ts index 87c68233f6..2fbd3b4585 100644 --- a/core/HoistProps.ts +++ b/core/HoistProps.ts @@ -11,7 +11,7 @@ import {CSSProperties, HTMLAttributes, LegacyRef, ReactNode, Ref} from 'react'; /** * Props interfaces for Hoist Components. * - * This interface brings in additional properties that are added to the props + * These interfaces bring in additional properties that are added to the props * collection by HoistComponent. */ export type HoistPropsWithRef = HoistProps; From 8feec5419226ded0786a0cc6bd4f15ede9c7d742 Mon Sep 17 00:00:00 2001 From: Greg Solomon Date: Mon, 10 Jun 2024 09:57:37 -0400 Subject: [PATCH 14/23] Changes from CR w/ Lee --- .../userData/roles/warning/WarningBanner.ts | 4 +-- cmp/layout/Spacer.ts | 2 +- cmp/spinner/Spinner.ts | 4 +-- cmp/websocket/WebSocketIndicator.ts | 4 +-- core/AppSpec.ts | 4 +-- core/HoistComponent.ts | 33 ++++++++++++------- core/HoistProps.ts | 27 +++++++-------- desktop/cmp/appbar/AppBar.ts | 4 +-- desktop/cmp/button/ButtonGroup.ts | 2 +- desktop/cmp/contextmenu/ContextMenu.ts | 2 +- desktop/cmp/filter/FilterChooser.ts | 10 +++--- desktop/cmp/form/FormField.ts | 15 ++------- desktop/cmp/input/ButtonGroupInput.ts | 3 +- desktop/cmp/panel/impl/PanelHeader.ts | 4 +-- desktop/cmp/panel/impl/Splitter.ts | 4 +-- desktop/cmp/toolbar/Toolbar.ts | 6 ++-- desktop/cmp/toolbar/ToolbarSep.ts | 4 +-- icon/XHLogo.tsx | 4 +-- icon/impl/IconCmp.ts | 4 +-- kit/blueprint/Dialog.ts | 8 ++--- mobile/cmp/dialog/Dialog.ts | 2 +- mobile/cmp/form/FormField.ts | 14 ++------ mobile/cmp/panel/DialogPanel.ts | 2 +- mobile/cmp/toolbar/ToolbarSeparator.ts | 4 +-- 24 files changed, 78 insertions(+), 92 deletions(-) diff --git a/admin/tabs/userData/roles/warning/WarningBanner.ts b/admin/tabs/userData/roles/warning/WarningBanner.ts index d28ee0a23a..6f1500ede2 100644 --- a/admin/tabs/userData/roles/warning/WarningBanner.ts +++ b/admin/tabs/userData/roles/warning/WarningBanner.ts @@ -5,12 +5,12 @@ * Copyright © 2024 Extremely Heavy Industries Inc. */ import {div} from '@xh/hoist/cmp/layout'; -import {hoistCmp, HoistProps, NoModel} from '@xh/hoist/core'; +import {hoistCmp, HoistProps} from '@xh/hoist/core'; import {toolbar} from '@xh/hoist/desktop/cmp/toolbar'; import {Icon} from '@xh/hoist/icon'; import './WarningBanner.scss'; -export interface WarningBannerProps extends HoistProps { +export interface WarningBannerProps extends HoistProps { compact?: boolean; message: string; } diff --git a/cmp/layout/Spacer.ts b/cmp/layout/Spacer.ts index a256b47315..9844fc14c9 100644 --- a/cmp/layout/Spacer.ts +++ b/cmp/layout/Spacer.ts @@ -31,7 +31,7 @@ export const [Spacer, spacer] = hoistCmp.withFactory({ /** * A component that stretches to soak up space along the main axis of its parent container. */ -export const [Filler, filler] = hoistCmp.withFactory>({ +export const [Filler, filler] = hoistCmp.withFactory({ displayName: 'Filler', model: false, observer: false, diff --git a/cmp/spinner/Spinner.ts b/cmp/spinner/Spinner.ts index 1d37b7a8c9..24032f9262 100644 --- a/cmp/spinner/Spinner.ts +++ b/cmp/spinner/Spinner.ts @@ -5,7 +5,7 @@ * Copyright © 2024 Extremely Heavy Industries Inc. */ import {img} from '@xh/hoist/cmp/layout'; -import {hoistCmp, HoistProps, NoModel} from '@xh/hoist/core'; +import {hoistCmp, HoistProps} from '@xh/hoist/core'; import {ImgHTMLAttributes} from 'react'; // @ts-ignore @@ -13,7 +13,7 @@ import compactSpinnerImg from './spinner-20px.png'; // @ts-ignore import spinnerImg from './spinner-50px.png'; -export interface SpinnerProps extends HoistProps, ImgHTMLAttributes { +export interface SpinnerProps extends HoistProps, ImgHTMLAttributes { /** True to return a smaller 20px image vs default 50px. */ compact?: boolean; } diff --git a/cmp/websocket/WebSocketIndicator.ts b/cmp/websocket/WebSocketIndicator.ts index 89a55524fa..3fcd71ac61 100644 --- a/cmp/websocket/WebSocketIndicator.ts +++ b/cmp/websocket/WebSocketIndicator.ts @@ -5,11 +5,11 @@ * Copyright © 2024 Extremely Heavy Industries Inc. */ import {hbox, span} from '@xh/hoist/cmp/layout'; -import {BoxProps, hoistCmp, HoistProps, NoModel, XH} from '@xh/hoist/core'; +import {BoxProps, hoistCmp, HoistProps, XH} from '@xh/hoist/core'; import {fmtTime} from '@xh/hoist/format'; import {Icon} from '@xh/hoist/icon'; -export interface WebSocketIndicatorProps extends HoistProps, BoxProps { +export interface WebSocketIndicatorProps extends HoistProps, BoxProps { /** True to display status as an icon only, without text label. */ iconOnly?: boolean; } diff --git a/core/AppSpec.ts b/core/AppSpec.ts index 97a7ace088..b4bedc5c05 100644 --- a/core/AppSpec.ts +++ b/core/AppSpec.ts @@ -4,7 +4,7 @@ * * Copyright © 2024 Extremely Heavy Industries Inc. */ -import {XH, HoistAppModel, ElementFactory, HoistProps} from '@xh/hoist/core'; +import {XH, HoistAppModel, ElementFactory, HoistProps, HoistPropsWithModel} from '@xh/hoist/core'; import {throwIf} from '@xh/hoist/utils/js'; import {isFunction, isNil, isString} from 'lodash'; import {Component, ComponentClass, FunctionComponent} from 'react'; @@ -47,7 +47,7 @@ export class AppSpec { * This class determines the platform used by Hoist. The value should be imported from * either `@xh/hoist/desktop/AppContainer` or `@xh/hoist/mobile/AppContainer`. */ - containerClass: ComponentClass | FunctionComponent; + containerClass: ComponentClass | FunctionComponent; /** True if the app should use the Hoist mobile toolkit.*/ isMobileApp: boolean; diff --git a/core/HoistComponent.ts b/core/HoistComponent.ts index b831372b80..a3a1693a85 100644 --- a/core/HoistComponent.ts +++ b/core/HoistComponent.ts @@ -15,7 +15,8 @@ import { TestSupportProps, ModelTypeOf, RefTypeOf, - PlainObject + PlainObject, + HoistPropsWithModel } from './'; import { useModelLinker, @@ -68,12 +69,13 @@ export type ComponentConfig< /** HoistProps used to infer model and ref types */ P extends HoistProps, RefTypeOf

>, /** Additional props that may be passed to the render function */ - D extends PlainObject = {} + D extends PlainObject = {}, + R = RefTypeOf

extends never ? unknown : RefTypeOf

> = - | ((props: RenderPropsOf

& D, ref?: ForwardedRef>) => ReactNode) + | ((props: RenderPropsOf

& D, ref?: ForwardedRef) => ReactNode) | { /** Render function defining the component. */ - render(props: RenderPropsOf

& D, ref?: ForwardedRef>): ReactNode; + render(props: RenderPropsOf

& D, ref?: ForwardedRef): ReactNode; /** * Spec defining the model to be rendered by this component. @@ -81,7 +83,7 @@ export type ComponentConfig< * return of {@link uses} or {@link creates} - these factory functions will create a spec for * either externally-provided or internally-created models. Defaults to `uses('*')`. */ - model?: ModelTypeOf

extends null ? false : ModelSpec>; + model?: ModelTypeOf

extends never ? false : ModelSpec>; /** * Base CSS class for this component. Will be combined with any className @@ -139,7 +141,7 @@ let cmpIndex = 0; // index for anonymous component dispay names * - `hoistCmp.withFactory` - return a 2-element list containing both the newly * defined Component and an elementFactory for it. */ -export function hoistCmp( +export function hoistCmp( config: ComponentConfig, PlainObject> // Infer model, but accept all props ): FC>; export function hoistCmp

, RefTypeOf

>>( @@ -206,7 +208,7 @@ export const hoistComponent = hoistCmp; * * Most typically used by application, this provides a simple element factory. */ -export function hoistCmpFactory( +export function hoistCmpFactory( config: ComponentConfig, PlainObject> // Infer model, but accept all props ): ElementFactory>; export function hoistCmpFactory

, RefTypeOf

>>( @@ -223,13 +225,13 @@ hoistCmp.factory = hoistCmpFactory; * * Not typically used by applications. */ -export function hoistCmpWithFactory( +export function hoistCmpWithFactory( config: ComponentConfig, PlainObject> // Infer model, but accept all props -): [FC>, ElementFactory>]; +): [FC>, ElementFactory>]; export function hoistCmpWithFactory

, RefTypeOf

>>( config: ComponentConfig

): [FC

, ElementFactory

]; -export function hoistCmpWithFactory(config) { +export function hoistCmpWithFactory(config: ComponentConfig>) { const cmp = hoistCmp(config); return [cmp, elementFactory(cmp)]; } @@ -242,7 +244,10 @@ hoistCmp.withFactory = hoistCmpWithFactory; //---------------------------------- // internal types and core wrappers //---------------------------------- -type RenderFn = (props: HoistProps & TestSupportProps, ref?: ForwardedRef) => ReactNode; +type RenderFn = ( + props: HoistPropsWithModel & TestSupportProps, + ref?: ForwardedRef +) => ReactNode; interface Config { displayName: string; @@ -401,7 +406,11 @@ function createModel(spec: CreatesSpec): ResolvedModel { return {model, isLinked: true, fromContext: false}; } -function lookupModel(props: HoistProps, modelLookup: ModelLookup, cfg: Config): ResolvedModel { +function lookupModel( + props: HoistPropsWithModel, + modelLookup: ModelLookup, + cfg: Config +): ResolvedModel { let {model, modelConfig} = props, spec = cfg.modelSpec as UsesSpec, selector = spec.selector as any; diff --git a/core/HoistProps.ts b/core/HoistProps.ts index 2fbd3b4585..6656e777d4 100644 --- a/core/HoistProps.ts +++ b/core/HoistProps.ts @@ -14,27 +14,28 @@ import {CSSProperties, HTMLAttributes, LegacyRef, ReactNode, Ref} from 'react'; * These interfaces bring in additional properties that are added to the props * collection by HoistComponent. */ -export type HoistPropsWithRef = HoistProps; -export interface HoistProps { +export type HoistPropsWithModel = HoistProps; +export type HoistPropsWithRef = HoistProps; +export interface HoistProps { /** * Associated HoistModel for this Component. Depending on the component, may be specified as * an instance of a HoistModel or left undefined. * HoistComponent will resolve (i.e. lookup in context or create if needed) a concrete Model * instance and provide it to the Render method of the component. */ - model?: M extends null ? never : M; + model?: M; /** * Used for specifying the *configuration* of a model to be created by Hoist for this component * when first mounted. Should be used only on a component that specifies the 'uses()' directive * with the `createFromConfig` set as true. See the `uses()` directive for more information. */ - modelConfig?: M extends null ? never : M['config']; + modelConfig?: M extends never ? never : M['config']; /** * Used for gaining a reference to the model of a HoistComponent. */ - modelRef?: M extends null ? never : Ref; + modelRef?: M extends never ? never : Ref; /** * ClassName for the component. Includes the classname as provided in props, enhanced with @@ -49,22 +50,16 @@ export interface HoistProps { ref?: R extends never ? never : LegacyRef; } -/** Alias to be used when a component does not require a model. */ -export type NoModel = null; - /** Infer the Model type from a HoistProps type. */ -export type ModelTypeOf> = T extends null - ? null +export type ModelTypeOf> = T extends never + ? never : T extends HoistProps ? M - : null; + : never; /** Infer the Ref type from a HoistProps type. */ -export type RefTypeOf> = T extends null - ? never - : T extends HoistProps - ? R - : never; +export type RefTypeOf> = + T extends HoistProps ? R : never; /** Extract all non-model and non-ref props from a HoistProps type. */ export type WithoutModelAndRef> = Omit< diff --git a/desktop/cmp/appbar/AppBar.ts b/desktop/cmp/appbar/AppBar.ts index b0c3a8c95f..1b2d6f8128 100644 --- a/desktop/cmp/appbar/AppBar.ts +++ b/desktop/cmp/appbar/AppBar.ts @@ -6,7 +6,7 @@ */ import {span} from '@xh/hoist/cmp/layout'; -import {hoistCmp, HoistProps, HSide, NoModel, TestSupportProps, XH} from '@xh/hoist/core'; +import {hoistCmp, HoistProps, HSide, TestSupportProps, XH} from '@xh/hoist/core'; import {appBarSeparator} from '@xh/hoist/desktop/cmp/appbar'; import {appMenuButton, AppMenuButtonProps, refreshButton} from '@xh/hoist/desktop/cmp/button'; import {whatsNewButton} from '@xh/hoist/desktop/cmp/button/WhatsNewButton'; @@ -17,7 +17,7 @@ import {isEmpty} from 'lodash'; import {ReactElement, ReactNode} from 'react'; import './AppBar.scss'; -export interface AppBarProps extends HoistProps, TestSupportProps { +export interface AppBarProps extends HoistProps, TestSupportProps { /** Position of the AppMenuButton. */ appMenuButtonPosition?: HSide; diff --git a/desktop/cmp/button/ButtonGroup.ts b/desktop/cmp/button/ButtonGroup.ts index 664e2b912b..cbe089bf24 100644 --- a/desktop/cmp/button/ButtonGroup.ts +++ b/desktop/cmp/button/ButtonGroup.ts @@ -19,7 +19,7 @@ import {TEST_ID} from '@xh/hoist/utils/js'; import {splitLayoutProps} from '@xh/hoist/utils/react'; import {SetOptional} from 'type-fest'; -export interface ButtonGroupProps +export interface ButtonGroupProps extends HoistProps, LayoutProps, StyleProps, diff --git a/desktop/cmp/contextmenu/ContextMenu.ts b/desktop/cmp/contextmenu/ContextMenu.ts index a8d35d6a26..f2f9cf562d 100644 --- a/desktop/cmp/contextmenu/ContextMenu.ts +++ b/desktop/cmp/contextmenu/ContextMenu.ts @@ -18,7 +18,7 @@ import {isValidElement, ReactElement, ReactNode} from 'react'; */ export type ContextMenuSpec = MenuItemLike[] | ((e: MouseEvent) => MenuItemLike[]) | ReactElement; -export interface ContextMenuProps extends HoistProps { +export interface ContextMenuProps extends HoistProps { menuItems: MenuItemLike[]; } diff --git a/desktop/cmp/filter/FilterChooser.ts b/desktop/cmp/filter/FilterChooser.ts index 663ce0f0b8..63cc9d31ee 100644 --- a/desktop/cmp/filter/FilterChooser.ts +++ b/desktop/cmp/filter/FilterChooser.ts @@ -6,7 +6,7 @@ */ import {FilterChooserFilter, FilterChooserModel} from '@xh/hoist/cmp/filter'; import {box, div, hbox, hframe, vbox} from '@xh/hoist/cmp/layout'; -import {hoistCmp, HoistProps, LayoutProps, NoModel, uses} from '@xh/hoist/core'; +import {hoistCmp, HoistProps, LayoutProps, uses} from '@xh/hoist/core'; import {button} from '@xh/hoist/desktop/cmp/button'; import {select} from '@xh/hoist/desktop/cmp/input'; import '@xh/hoist/desktop/register'; @@ -129,7 +129,7 @@ function optionRenderer(opt) { return null; } -const fieldOption = hoistCmp.factory({ +const fieldOption = hoistCmp.factory({ model: false, observer: false, memo: false, @@ -147,7 +147,7 @@ const fieldOption = hoistCmp.factory({ } }); -const minimalFieldOption = hoistCmp.factory({ +const minimalFieldOption = hoistCmp.factory({ model: false, observer: false, memo: false, @@ -160,7 +160,7 @@ const minimalFieldOption = hoistCmp.factory({ } }); -const filterOption = hoistCmp.factory({ +const filterOption = hoistCmp.factory({ model: false, observer: false, render({fieldSpec, displayOp, displayValue}) { @@ -175,7 +175,7 @@ const filterOption = hoistCmp.factory({ } }); -const messageOption = hoistCmp.factory({ +const messageOption = hoistCmp.factory({ model: false, observer: false, render({label}) { diff --git a/desktop/cmp/form/FormField.ts b/desktop/cmp/form/FormField.ts index 56d5a41bb8..33fe46f846 100644 --- a/desktop/cmp/form/FormField.ts +++ b/desktop/cmp/form/FormField.ts @@ -8,16 +8,7 @@ import {PopoverPosition, PopperBoundary} from '@blueprintjs/core'; import composeRefs from '@seznam/compose-react-refs/composeRefs'; import {BaseFormFieldProps, FieldModel, FormContext, FormContextType} from '@xh/hoist/cmp/form'; import {box, div, label as labelEl, li, span, ul} from '@xh/hoist/cmp/layout'; -import { - hoistCmp, - HoistProps, - HSide, - NoModel, - PlainObject, - TestSupportProps, - uses, - XH -} from '@xh/hoist/core'; +import {hoistCmp, HoistProps, HSide, PlainObject, TestSupportProps, uses, XH} from '@xh/hoist/core'; import '@xh/hoist/desktop/register'; import {instanceManager} from '@xh/hoist/core/impl/InstanceManager'; import {fmtDate, fmtDateTime, fmtJson, fmtNumber} from '@xh/hoist/format'; @@ -253,7 +244,7 @@ export const [FormField, formField] = hoistCmp.withFactory({ } }); -interface ReadonlyChildProps extends HoistProps, TestSupportProps { +interface ReadonlyChildProps extends HoistProps, TestSupportProps { fieldModel: FieldModel; readonlyRenderer: (v: any, model: FieldModel) => ReactNode; } @@ -271,7 +262,7 @@ const readonlyChild = hoistCmp.factory({ } }); -const editableChild = hoistCmp.factory({ +const editableChild = hoistCmp.factory({ model: false, render({ diff --git a/desktop/cmp/input/ButtonGroupInput.ts b/desktop/cmp/input/ButtonGroupInput.ts index d34828ec26..10cd6a1b4b 100644 --- a/desktop/cmp/input/ButtonGroupInput.ts +++ b/desktop/cmp/input/ButtonGroupInput.ts @@ -6,9 +6,8 @@ */ import {HoistInputModel, HoistInputProps, useHoistInputModel} from '@xh/hoist/cmp/input'; import {hoistCmp, Intent, WithoutModelAndRef, XH} from '@xh/hoist/core'; -import {Button, buttonGroup, ButtonGroupProps} from '@xh/hoist/desktop/cmp/button'; +import {Button, buttonGroup, ButtonGroupProps, ButtonProps} from '@xh/hoist/desktop/cmp/button'; import '@xh/hoist/desktop/register'; -import {ButtonProps} from '@xh/hoist/desktop/cmp/button'; import {throwIf, warnIf, withDefault} from '@xh/hoist/utils/js'; import {getLayoutProps, getNonLayoutProps} from '@xh/hoist/utils/react'; import {castArray, filter, isEmpty, without} from 'lodash'; diff --git a/desktop/cmp/panel/impl/PanelHeader.ts b/desktop/cmp/panel/impl/PanelHeader.ts index ed020488a8..1e2eeadbb3 100644 --- a/desktop/cmp/panel/impl/PanelHeader.ts +++ b/desktop/cmp/panel/impl/PanelHeader.ts @@ -5,7 +5,7 @@ * Copyright © 2024 Extremely Heavy Industries Inc. */ import {box, filler, hbox, span, vbox} from '@xh/hoist/cmp/layout'; -import {hoistCmp, NoModel, useContextModel} from '@xh/hoist/core'; +import {hoistCmp, useContextModel} from '@xh/hoist/core'; import {button, modalToggleButton} from '@xh/hoist/desktop/cmp/button'; import {Icon} from '@xh/hoist/icon'; import {withDefault} from '@xh/hoist/utils/js'; @@ -14,7 +14,7 @@ import {isEmpty, isNil} from 'lodash'; import {PanelModel} from '../PanelModel'; import './PanelHeader.scss'; -export const panelHeader = hoistCmp.factory({ +export const panelHeader = hoistCmp.factory({ displayName: 'PanelHeader', model: false, className: 'xh-panel-header', diff --git a/desktop/cmp/panel/impl/Splitter.ts b/desktop/cmp/panel/impl/Splitter.ts index 9ecf0bf2fc..fc67464244 100644 --- a/desktop/cmp/panel/impl/Splitter.ts +++ b/desktop/cmp/panel/impl/Splitter.ts @@ -5,13 +5,13 @@ * Copyright © 2024 Extremely Heavy Industries Inc. */ import {hbox, vbox} from '@xh/hoist/cmp/layout'; -import {hoistCmp, NoModel, useContextModel} from '@xh/hoist/core'; +import {hoistCmp, useContextModel} from '@xh/hoist/core'; import {button} from '@xh/hoist/desktop/cmp/button'; import {Icon} from '@xh/hoist/icon'; import {PanelModel} from '../PanelModel'; import './Splitter.scss'; -export const splitter = hoistCmp.factory({ +export const splitter = hoistCmp.factory({ displayName: 'Splitter', model: false, diff --git a/desktop/cmp/toolbar/Toolbar.ts b/desktop/cmp/toolbar/Toolbar.ts index a168e796ac..0ce53fcb40 100644 --- a/desktop/cmp/toolbar/Toolbar.ts +++ b/desktop/cmp/toolbar/Toolbar.ts @@ -5,7 +5,7 @@ * Copyright © 2024 Extremely Heavy Industries Inc. */ import {filler, fragment, hbox, vbox} from '@xh/hoist/cmp/layout'; -import {BoxProps, hoistCmp, HoistPropsWithRef, NoModel} from '@xh/hoist/core'; +import {BoxProps, hoistCmp, HoistPropsWithRef} from '@xh/hoist/core'; import {button} from '@xh/hoist/desktop/cmp/button'; import '@xh/hoist/desktop/register'; import {Icon} from '@xh/hoist/icon'; @@ -101,7 +101,7 @@ export const [Toolbar, toolbar] = hoistCmp.withFactory({ //----------------- // Implementation //-------------- -const overflowBox = hoistCmp.factory({ +const overflowBox = hoistCmp.factory({ model: false, observer: false, memo: false, @@ -116,7 +116,7 @@ const overflowBox = hoistCmp.factory({ } }); -const overflowButton = hoistCmp.factory({ +const overflowButton = hoistCmp.factory({ model: false, observer: false, memo: false, diff --git a/desktop/cmp/toolbar/ToolbarSep.ts b/desktop/cmp/toolbar/ToolbarSep.ts index b4516ede17..be71671572 100644 --- a/desktop/cmp/toolbar/ToolbarSep.ts +++ b/desktop/cmp/toolbar/ToolbarSep.ts @@ -5,14 +5,14 @@ * Copyright © 2024 Extremely Heavy Industries Inc. */ import {span} from '@xh/hoist/cmp/layout'; -import {hoistCmp, NoModel} from '@xh/hoist/core'; +import {hoistCmp} from '@xh/hoist/core'; import '@xh/hoist/desktop/register'; import './Toolbar.scss'; /** * Convenience component to insert a pre-styled separator | between Toolbar items. */ -export const [ToolbarSeparator, toolbarSeparator] = hoistCmp.withFactory({ +export const [ToolbarSeparator, toolbarSeparator] = hoistCmp.withFactory({ displayName: 'ToolbarSeparator', model: false, observer: false, diff --git a/icon/XHLogo.tsx b/icon/XHLogo.tsx index c2e8582ae9..22d05141a7 100644 --- a/icon/XHLogo.tsx +++ b/icon/XHLogo.tsx @@ -4,14 +4,14 @@ * * Copyright © 2024 Extremely Heavy Industries Inc. */ -import {hoistCmp, NoModel, XH} from '@xh/hoist/core'; +import {hoistCmp, XH} from '@xh/hoist/core'; import {getLayoutProps} from '@xh/hoist/utils/react'; import React from 'react'; /** * The Extremely Heavy corporate XH logo and word mark, in SVG form. */ -export const xhLogo = hoistCmp.factory({ +export const xhLogo = hoistCmp.factory({ model: false, render(props) { diff --git a/icon/impl/IconCmp.ts b/icon/impl/IconCmp.ts index 63078873ed..16728442ae 100644 --- a/icon/impl/IconCmp.ts +++ b/icon/impl/IconCmp.ts @@ -5,7 +5,7 @@ * Copyright © 2024 Extremely Heavy Industries Inc. */ import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; -import {elementFactory, hoistCmp, NoModel} from '@xh/hoist/core'; +import {elementFactory, hoistCmp} from '@xh/hoist/core'; import {enhanceFaClasses} from './IconHtml'; /** @@ -13,7 +13,7 @@ import {enhanceFaClasses} from './IconHtml'; * Applications should use the factory methods on Icon instead. * @internal */ -export const iconCmp = hoistCmp.factory({ +export const iconCmp = hoistCmp.factory({ displayName: 'Icon', observer: false, model: false, diff --git a/kit/blueprint/Dialog.ts b/kit/blueprint/Dialog.ts index dc8a54125e..356229d60e 100644 --- a/kit/blueprint/Dialog.ts +++ b/kit/blueprint/Dialog.ts @@ -5,12 +5,12 @@ * Copyright © 2024 Extremely Heavy Industries Inc. */ import {div} from '@xh/hoist/cmp/layout'; -import {NoModel, hoistCmp} from '@xh/hoist/core'; +import {hoistCmp} from '@xh/hoist/core'; /** * Dialog Body for Blueprint, wrapped as a Hoist Component. */ -export const [DialogBody, dialogBody] = hoistCmp.withFactory({ +export const [DialogBody, dialogBody] = hoistCmp.withFactory({ displayName: 'DialogBody', className: 'bp5-dialog-body', observer: false, @@ -25,7 +25,7 @@ export const [DialogBody, dialogBody] = hoistCmp.withFactory({ /** * Dialog Footer for Blueprint, wrapped as Hoist Component. */ -export const [DialogFooter, dialogFooter] = hoistCmp.withFactory({ +export const [DialogFooter, dialogFooter] = hoistCmp.withFactory({ displayName: 'DialogFooter', className: 'bp5-dialog-footer', observer: false, @@ -40,7 +40,7 @@ export const [DialogFooter, dialogFooter] = hoistCmp.withFactory({ /** * Dialog Footer Actions for Blueprint, wrapped as HoistComponent. */ -export const [DialogFooterActions, dialogFooterActions] = hoistCmp.withFactory({ +export const [DialogFooterActions, dialogFooterActions] = hoistCmp.withFactory({ displayName: 'DialogFooterActions', className: 'bp5-dialog-footer-actions', observer: false, diff --git a/mobile/cmp/dialog/Dialog.ts b/mobile/cmp/dialog/Dialog.ts index f1c7ac70bd..48bb3fbb40 100644 --- a/mobile/cmp/dialog/Dialog.ts +++ b/mobile/cmp/dialog/Dialog.ts @@ -11,7 +11,7 @@ import '@xh/hoist/mobile/register'; import {ReactElement, ReactNode} from 'react'; import './Dialog.scss'; -export interface DialogProps extends HoistProps { +export interface DialogProps extends HoistProps { isOpen?: boolean; isCancelable?: boolean; onCancel?: () => void; diff --git a/mobile/cmp/form/FormField.ts b/mobile/cmp/form/FormField.ts index 40ddda2ff9..933e40c5f1 100644 --- a/mobile/cmp/form/FormField.ts +++ b/mobile/cmp/form/FormField.ts @@ -7,15 +7,7 @@ import composeRefs from '@seznam/compose-react-refs/composeRefs'; import {FieldModel, FormContext, FormContextType, BaseFormFieldProps} from '@xh/hoist/cmp/form'; import {box, div, span} from '@xh/hoist/cmp/layout'; -import { - hoistCmp, - HoistProps, - NoModel, - PlainObject, - TestSupportProps, - uses, - XH -} from '@xh/hoist/core'; +import {hoistCmp, HoistProps, PlainObject, TestSupportProps, uses, XH} from '@xh/hoist/core'; import {fmtDate, fmtDateTime, fmtNumber} from '@xh/hoist/format'; import {label as labelCmp} from '@xh/hoist/mobile/cmp/input'; import '@xh/hoist/mobile/register'; @@ -164,7 +156,7 @@ export const [FormField, formField] = hoistCmp.withFactory({ } }); -interface ReadonlyChildProps extends HoistProps, TestSupportProps { +interface ReadonlyChildProps extends HoistProps, TestSupportProps { fieldModel: FieldModel; readonlyRenderer: (v: any, model: FieldModel) => ReactNode; } @@ -181,7 +173,7 @@ const readonlyChild = hoistCmp.factory({ } }); -const editableChild = hoistCmp.factory({ +const editableChild = hoistCmp.factory({ model: false, render({child, childIsSizeable, disabled, commitOnChange, fieldModel, width, height, flex}) { diff --git a/mobile/cmp/panel/DialogPanel.ts b/mobile/cmp/panel/DialogPanel.ts index 379ee006d7..dd31463d41 100644 --- a/mobile/cmp/panel/DialogPanel.ts +++ b/mobile/cmp/panel/DialogPanel.ts @@ -10,7 +10,7 @@ import '@xh/hoist/mobile/register'; import './DialogPanel.scss'; import {panel, PanelProps} from './Panel'; -export interface DialogPanelProps extends HoistProps, WithoutModelAndRef { +export interface DialogPanelProps extends HoistProps, WithoutModelAndRef { /** Is the dialog panel shown. */ isOpen?: boolean; } diff --git a/mobile/cmp/toolbar/ToolbarSeparator.ts b/mobile/cmp/toolbar/ToolbarSeparator.ts index 07190a521a..bea0195d85 100644 --- a/mobile/cmp/toolbar/ToolbarSeparator.ts +++ b/mobile/cmp/toolbar/ToolbarSeparator.ts @@ -5,14 +5,14 @@ * Copyright © 2024 Extremely Heavy Industries Inc. */ import {span} from '@xh/hoist/cmp/layout'; -import {hoistCmp, NoModel} from '@xh/hoist/core'; +import {hoistCmp} from '@xh/hoist/core'; import '@xh/hoist/mobile/register'; import './Toolbar.scss'; /** * Convenience component to insert a pre-styled separator | between Toolbar items. */ -export const [ToolbarSeparator, toolbarSeparator] = hoistCmp.withFactory({ +export const [ToolbarSeparator, toolbarSeparator] = hoistCmp.withFactory({ displayName: 'ToolbarSeparator', className: 'xh-toolbar__separator', model: false, From 596aca9c3a07beb37239f19178e47ece66666afe Mon Sep 17 00:00:00 2001 From: Greg Solomon Date: Thu, 27 Jun 2024 09:39:48 -0400 Subject: [PATCH 15/23] Minor cleanup --- cmp/store/StoreFilterField.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmp/store/StoreFilterField.ts b/cmp/store/StoreFilterField.ts index 165e632d08..dd444b99bc 100644 --- a/cmp/store/StoreFilterField.ts +++ b/cmp/store/StoreFilterField.ts @@ -58,7 +58,7 @@ export interface StoreFilterFieldProps matchMode?: 'start' | 'startWord' | 'any'; /** Optional model for raw value binding - see comments on the `bind` prop for details. */ - model?: M extends null ? never : M; + model?: M; /** * Callback to receive an updated Filter. Typically used in conjunction with `autoApply: false` From 6182cd6521b59cbb0e367920c7a91bc42feb2d0c Mon Sep 17 00:00:00 2001 From: Greg Solomon Date: Thu, 27 Jun 2024 17:32:04 -0400 Subject: [PATCH 16/23] Infer prop types for components passed to elementFactory --- CHANGELOG.md | 3 +- admin/differ/DifferDetail.ts | 2 +- cmp/error/ErrorBoundary.ts | 4 +- cmp/filter/FilterChooserModel.ts | 2 +- cmp/grid/filter/GridFilterFieldSpec.ts | 2 +- cmp/grouping/GroupingChooserModel.ts | 2 +- cmp/input/HoistInputModel.ts | 15 +-- cmp/input/HoistInputProps.ts | 12 ++- cmp/layout/Tags.ts | 63 +++++++------ cmp/tab/TabModel.ts | 5 +- core/elem.ts | 14 ++- desktop/appcontainer/ToastSource.ts | 7 +- desktop/cmp/button/AppMenuButton.ts | 7 +- desktop/cmp/button/ButtonGroup.ts | 2 +- desktop/cmp/contextmenu/ContextMenu.ts | 4 +- .../cmp/dash/canvas/impl/DashCanvasView.ts | 3 +- desktop/cmp/filechooser/FileChooser.ts | 4 +- desktop/cmp/form/FormField.ts | 2 +- desktop/cmp/grid/editors/EditorProps.ts | 2 +- .../grid/editors/impl/InlineEditorModel.ts | 2 +- .../grid/impl/filter/ColumnHeaderFilter.ts | 1 - desktop/cmp/grouping/GroupingChooser.ts | 5 +- desktop/cmp/input/ButtonGroupInput.ts | 4 +- desktop/cmp/input/Checkbox.ts | 57 +++++------ desktop/cmp/input/CodeInput.ts | 6 +- desktop/cmp/input/DateInput.ts | 14 +-- desktop/cmp/input/NumberInput.ts | 18 ++-- desktop/cmp/input/RadioInput.ts | 7 +- desktop/cmp/input/Select.ts | 4 +- desktop/cmp/input/Slider.ts | 44 +++++---- desktop/cmp/input/SwitchInput.ts | 48 +++++----- desktop/cmp/input/TextArea.ts | 12 ++- desktop/cmp/input/TextInput.ts | 10 +- desktop/cmp/tab/TabSwitcher.ts | 3 +- desktop/cmp/toolbar/Toolbar.ts | 1 + kit/react-select/index.ts | 3 +- mobile/cmp/input/ButtonGroupInput.ts | 94 ++++++++++--------- mobile/cmp/input/Checkbox.ts | 4 +- mobile/cmp/input/CheckboxButton.ts | 6 +- mobile/cmp/input/DateInput.ts | 4 +- mobile/cmp/input/Label.ts | 25 ++--- mobile/cmp/input/NumberInput.ts | 4 +- mobile/cmp/input/SearchInput.ts | 4 +- mobile/cmp/input/Select.ts | 4 +- mobile/cmp/input/SwitchInput.ts | 4 +- mobile/cmp/input/TextArea.ts | 4 +- mobile/cmp/input/TextInput.ts | 4 +- 47 files changed, 304 insertions(+), 247 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ace0398654..74ad9bb772 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,7 +30,8 @@ if the defaults introduced here are suitable for your application's Hoist Admin console. * Typescript: Overall type improvements and cleanup. Note: `AppConfigs` with `model: false` will need to specify a `null` model type in the generic argument to `hoistCmp`, `hoistCmp.factory` or - `hoistCmp.withFacotry` to avoid a type error. + `hoistCmp.withFactory` to avoid a type error. Additionally, prop types for components passed to + `elementFactory` are now inferred from the component itself where possible. ### 📚 Libraries diff --git a/admin/differ/DifferDetail.ts b/admin/differ/DifferDetail.ts index 05b37163e7..4d20d02e7c 100644 --- a/admin/differ/DifferDetail.ts +++ b/admin/differ/DifferDetail.ts @@ -27,7 +27,7 @@ export const differDetail = hoistCmp.factory({ return dialog({ title: 'Detail', icon: Icon.diff(), - isOpen: model.record, + isOpen: !!model.record, className: 'xh-admin-diff-detail', onClose: () => model.close(), item: panel({ diff --git a/cmp/error/ErrorBoundary.ts b/cmp/error/ErrorBoundary.ts index 377e7e73b9..d684e6e3ef 100644 --- a/cmp/error/ErrorBoundary.ts +++ b/cmp/error/ErrorBoundary.ts @@ -28,10 +28,10 @@ export const [ErrorBoundary, errorBoundary] = hoistCmp.withFactory(); + inputRef = createObservableRef>(); constructor({ fieldSpecs, diff --git a/cmp/grid/filter/GridFilterFieldSpec.ts b/cmp/grid/filter/GridFilterFieldSpec.ts index b5901c26ef..c67cc22e95 100644 --- a/cmp/grid/filter/GridFilterFieldSpec.ts +++ b/cmp/grid/filter/GridFilterFieldSpec.ts @@ -29,7 +29,7 @@ export interface GridFilterFieldSpecConfig extends BaseFilterFieldSpecConfig { * Props to pass through to the HoistInput components used on the custom filter tab. * Note that the HoistInput component used is decided by fieldType. */ - inputProps?: HoistInputProps; + inputProps?: HoistInputProps; /** Default operator displayed in custom filter tab. */ defaultOp?: FieldFilterOperator; diff --git a/cmp/grouping/GroupingChooserModel.ts b/cmp/grouping/GroupingChooserModel.ts index 88b15859ab..6765aacd54 100644 --- a/cmp/grouping/GroupingChooserModel.ts +++ b/cmp/grouping/GroupingChooserModel.ts @@ -331,7 +331,7 @@ export class GroupingChooserModel extends HoistModel { } isFavorite(value: string[]) { - return this.favorites?.find(v => isEqual(v, value)); + return this.favorites?.some(v => isEqual(v, value)); } //------------------------- diff --git a/cmp/input/HoistInputModel.ts b/cmp/input/HoistInputModel.ts index 87a12e60ad..9fdb02b3c5 100644 --- a/cmp/input/HoistInputModel.ts +++ b/cmp/input/HoistInputModel.ts @@ -51,10 +51,13 @@ import './HoistInput.scss'; * element of the rendered input via its `domEl` property, as well as `focus()`, `blur()`, and * `select()`. * + * @typeparam R - the type of HoistInputModel.inputRef (if any) - used to specify the type of the + * ref passed to the component. + * * To create an instance of an Input component using this model use the hook * {@link useHoistInputModel}. */ -export class HoistInputModel extends HoistModel { +export class HoistInputModel extends HoistModel { /** Does this input have the focus? */ @observable hasFocus: boolean = false; @@ -102,7 +105,7 @@ export class HoistInputModel extends HoistModel { // Implementation State //------------------------ @observable.ref internalValue: any = null; // Cached internal value - inputRef = createObservableRef(); // ref to internal element, if any + inputRef = createObservableRef(); // ref to internal element, if any domRef = createObservableRef(); // ref to outermost element, or class Component. isDirty: boolean = false; @@ -326,13 +329,13 @@ export class HoistInputModel extends HoistModel { * @param ref - forwardRef passed to containing component * @param modelSpec - specify to use particular subclass of HoistInputModel */ -export function useHoistInputModel( +export function useHoistInputModel( component: any, props: PlainObject, - ref: ForwardedRef, - modelSpec?: HoistModelClass + ref: ForwardedRef>, + modelSpec?: HoistModelClass> ): ReactElement { - const inputModel = useLocalModel(modelSpec ?? HoistInputModel); + const inputModel = useLocalModel>(modelSpec ?? HoistInputModel); useImperativeHandle(ref, () => inputModel); diff --git a/cmp/input/HoistInputProps.ts b/cmp/input/HoistInputProps.ts index acc553cf07..52bcb3a542 100644 --- a/cmp/input/HoistInputProps.ts +++ b/cmp/input/HoistInputProps.ts @@ -7,8 +7,15 @@ import {HoistInputModel} from '@xh/hoist/cmp/input/HoistInputModel'; import {HoistModel, HoistProps, TestSupportProps} from '@xh/hoist/core'; +import {CSSProperties} from 'react'; -export interface HoistInputProps extends TestSupportProps, HoistProps { +/** + * Props for HoistInput components. + * @typeparam R - the type of HoistInputModel.inputRef (if any) for this component. + */ +export interface HoistInputProps + extends TestSupportProps, + HoistProps> { /** * Field or model property name from which this component should read and write its value * in controlled mode. Can be set by parent FormField. @@ -27,6 +34,9 @@ export interface HoistInputProps extends TestSupportProps, HoistProps void; + /** CSS style attributes for the input element. */ + style?: CSSProperties; + /** Tab order for focus control, or -1 to skip. If unset, browser layout-based order. */ tabIndex?: number; diff --git a/cmp/layout/Tags.ts b/cmp/layout/Tags.ts index 08c1ced823..4393e9d5fc 100644 --- a/cmp/layout/Tags.ts +++ b/cmp/layout/Tags.ts @@ -15,38 +15,39 @@ export const fragment = elementFactory(Fragment); //------------------------- // Leaf HTML Tags //------------------------- -export const iframe = elementFactory('iframe'); -export const img = elementFactory('img'); -export const input = elementFactory('input'); -export const svg = elementFactory('svg'); -export const textarea = elementFactory('textarea'); +export const iframe = elementFactory('iframe'); +export const img = elementFactory('img'); +export const input = elementFactory('input'); +export const svg = elementFactory('svg'); +export const textarea = elementFactory('textarea'); //-------------------------------- // Container HTML Tags //-------------------------------- -export const a = elementFactory('a'); -export const br = elementFactory('br'); -export const code = elementFactory('code'); -export const div = elementFactory('div'); -export const form = elementFactory('form'); -export const hr = elementFactory('hr'); -export const h1 = elementFactory('h1'); -export const h2 = elementFactory('h2'); -export const h3 = elementFactory('h3'); -export const h4 = elementFactory('h4'); -export const label = elementFactory('label'); -export const li = elementFactory('li'); -export const nav = elementFactory('nav'); -export const ol = elementFactory('ol'); -export const option = elementFactory('option'); -export const p = elementFactory('p'); -export const pre = elementFactory('pre'); -export const span = elementFactory('span'); -export const strong = elementFactory('strong'); -export const table = elementFactory('table'); -export const tbody = elementFactory('tbody'); -export const td = elementFactory('td'); -export const th = elementFactory('th'); -export const thead = elementFactory('thead'); -export const tr = elementFactory('tr'); -export const ul = elementFactory('ul'); + +export const a = elementFactory('a'); +export const br = elementFactory('br'); +export const code = elementFactory('code'); +export const div = elementFactory('div'); +export const form = elementFactory('form'); +export const hr = elementFactory('hr'); +export const h1 = elementFactory('h1'); +export const h2 = elementFactory('h2'); +export const h3 = elementFactory('h3'); +export const h4 = elementFactory('h4'); +export const label = elementFactory('label'); +export const li = elementFactory('li'); +export const nav = elementFactory('nav'); +export const ol = elementFactory('ol'); +export const option = elementFactory('option'); +export const p = elementFactory('p'); +export const pre = elementFactory('pre'); +export const span = elementFactory('span'); +export const strong = elementFactory('strong'); +export const table = elementFactory('table'); +export const tbody = elementFactory('tbody'); +export const td = elementFactory('td'); +export const th = elementFactory('th'); +export const thead = elementFactory('thead'); +export const tr = elementFactory('tr'); +export const ul = elementFactory('ul'); diff --git a/cmp/tab/TabModel.ts b/cmp/tab/TabModel.ts index 10b90ba720..fae88aa117 100644 --- a/cmp/tab/TabModel.ts +++ b/cmp/tab/TabModel.ts @@ -19,6 +19,7 @@ import {action, computed, observable, makeObservable, bindable} from '@xh/hoist/ import {throwIf} from '@xh/hoist/utils/js'; import {startCase} from 'lodash'; import {TabContainerModel} from '@xh/hoist/cmp/tab/TabContainerModel'; +import {JSX} from 'react'; import {ReactElement, ReactNode} from 'react'; export interface TabConfig { @@ -38,7 +39,7 @@ export interface TabConfig { icon?: ReactElement; /** Tooltip for the Tab in the container's TabSwitcher. */ - tooltip?: ReactNode; + tooltip?: JSX.Element | string; /** True to disable this tab in the TabSwitcher and block routing. */ disabled?: boolean; @@ -85,7 +86,7 @@ export class TabModel extends HoistModel { id: string; @bindable.ref title: ReactNode; @bindable.ref icon: ReactElement; - @bindable.ref tooltip: ReactNode; + @bindable.ref tooltip: JSX.Element | string; @observable disabled: boolean; @bindable excludeFromSwitcher: boolean; showRemoveAction: boolean; diff --git a/core/elem.ts b/core/elem.ts index f568f661a2..04c586ba49 100644 --- a/core/elem.ts +++ b/core/elem.ts @@ -7,8 +7,10 @@ import {TEST_ID} from '@xh/hoist/utils/js'; import {castArray, isFunction, isNil, isPlainObject} from 'lodash'; import { + ComponentType, createElement as reactCreateElement, isValidElement, + JSX, Key, ReactElement, ReactNode @@ -108,7 +110,10 @@ export function createElement

(type: any, spec: ElementSpec

): ReactEl /** * Create a factory function that can create a ReactElement from an ElementSpec. */ -export function elementFactory

(type: any): ElementFactory

{ +export function elementFactory< + T extends ComponentType | keyof JSX.IntrinsicElements, + P = PropType +>(type: T): ElementFactory

{ const ret = function (...args) { return createElement

(type, normalizeArgs(args, type)); }; @@ -130,3 +135,10 @@ function normalizeArgs(args: any[], type: any) { // Assume > 1 args are children. return {items: args}; } + +type PropType = + T extends ComponentType + ? P + : T extends keyof JSX.IntrinsicElements + ? JSX.IntrinsicElements[T] + : any; diff --git a/desktop/appcontainer/ToastSource.ts b/desktop/appcontainer/ToastSource.ts index 18b3983c4e..25693029dd 100644 --- a/desktop/appcontainer/ToastSource.ts +++ b/desktop/appcontainer/ToastSource.ts @@ -4,6 +4,7 @@ * * Copyright © 2024 Extremely Heavy Industries Inc. */ +import {OverlayToasterProps} from '@blueprintjs/core'; import {ToastModel} from '@xh/hoist/appcontainer/ToastModel'; import {ToastSourceModel} from '@xh/hoist/appcontainer/ToastSourceModel'; import {div} from '@xh/hoist/cmp/layout'; @@ -21,6 +22,7 @@ import {OverlayToaster, ToasterPosition} from '@xh/hoist/kit/blueprint'; import {getOrCreate} from '@xh/hoist/utils/js'; import classNames from 'classnames'; import {isElement, map} from 'lodash'; +import {RefAttributes} from 'react'; import {createRoot} from 'react-dom/client'; import {wait} from '../../promise'; import './Toast.scss'; @@ -138,4 +140,7 @@ class ToastSourceLocalModel extends HoistModel { } } -const overlayToaster = elementFactory(OverlayToaster); +// `OverlayToasterProps` does not include `ref` prop, so we need to add it manually +const overlayToaster = elementFactory>( + OverlayToaster +); diff --git a/desktop/cmp/button/AppMenuButton.ts b/desktop/cmp/button/AppMenuButton.ts index 79c82052f3..1b50be8416 100644 --- a/desktop/cmp/button/AppMenuButton.ts +++ b/desktop/cmp/button/AppMenuButton.ts @@ -4,7 +4,8 @@ * * Copyright © 2024 Extremely Heavy Industries Inc. */ -import {hoistCmp, MenuItemLike, MenuItem, PlainObject, XH} from '@xh/hoist/core'; +import {MenuItemProps} from '@blueprintjs/core'; +import {hoistCmp, MenuItemLike, MenuItem, XH, ElementSpec} from '@xh/hoist/core'; import {ButtonProps, button} from '@xh/hoist/desktop/cmp/button'; import '@xh/hoist/desktop/register'; import {Icon} from '@xh/hoist/icon'; @@ -196,14 +197,14 @@ function parseMenuItems(items: MenuItemLike[]): ReactNode[] { const {actionFn} = item; // Create menuItem from config - const cfg = { + const cfg: ElementSpec = { text: item.text, icon: item.icon, intent: item.intent, className: item.className, onClick: actionFn ? () => wait().then(actionFn) : null, // do async to allow menu to close disabled: item.disabled - } as PlainObject; + }; // Recursively parse any submenus if (!isEmpty(item.items)) { diff --git a/desktop/cmp/button/ButtonGroup.ts b/desktop/cmp/button/ButtonGroup.ts index cbe089bf24..b9b1f9f127 100644 --- a/desktop/cmp/button/ButtonGroup.ts +++ b/desktop/cmp/button/ButtonGroup.ts @@ -56,7 +56,7 @@ export const [ButtonGroup, buttonGroup] = hoistCmp.withFactory ...layoutProps }, ref, - ...rest + ...(rest as BpButtonGroupProps) }); } }); diff --git a/desktop/cmp/contextmenu/ContextMenu.ts b/desktop/cmp/contextmenu/ContextMenu.ts index f2f9cf562d..d6c6ef0e65 100644 --- a/desktop/cmp/contextmenu/ContextMenu.ts +++ b/desktop/cmp/contextmenu/ContextMenu.ts @@ -38,8 +38,8 @@ export const [ContextMenu, contextMenu] = hoistCmp.withFactory observer: false, render({menuItems}) { - menuItems = parseItems(menuItems); - return isEmpty(menuItems) ? null : menu(menuItems); + const items = parseItems(menuItems); + return isEmpty(items) ? null : menu(items); } }); diff --git a/desktop/cmp/dash/canvas/impl/DashCanvasView.ts b/desktop/cmp/dash/canvas/impl/DashCanvasView.ts index fd75217092..a9c6f348eb 100644 --- a/desktop/cmp/dash/canvas/impl/DashCanvasView.ts +++ b/desktop/cmp/dash/canvas/impl/DashCanvasView.ts @@ -15,6 +15,7 @@ import {button} from '../../../button'; import {panel} from '../../../panel'; import {DashCanvasViewModel} from '../DashCanvasViewModel'; import {errorBoundary} from '@xh/hoist/cmp/error/ErrorBoundary'; +import {ReactElement} from 'react'; /** * Implementation component to show an item within a DashCanvas. This component @@ -114,7 +115,7 @@ const headerMenu = hoistCmp.factory(({model}) => { '-', ...(containerModel.extraMenuItems ?? []) ] - }); + }) as ReactElement; // Workaround using React functional component to check if ContextMenu renders null // TODO - build a popover wrapper that null-checks content instead of this workaround diff --git a/desktop/cmp/filechooser/FileChooser.ts b/desktop/cmp/filechooser/FileChooser.ts index 0be611079d..2f0de358e9 100644 --- a/desktop/cmp/filechooser/FileChooser.ts +++ b/desktop/cmp/filechooser/FileChooser.ts @@ -88,7 +88,9 @@ export const [FileChooser, fileChooser] = hoistCmp.withFactory maxSize, minSize, multiple: enableAddMulti, - item: ({getRootProps, getInputProps, isDragActive, draggedFiles}) => { + // Passing children directly since it is not possible to pass a function via + // elementFactory items prop. + children: ({getRootProps, getInputProps, isDragActive, draggedFiles}) => { const draggedCount = draggedFiles.length, targetTxt = isDragActive ? `Drop to add ${fileNoun(draggedCount)}.` diff --git a/desktop/cmp/form/FormField.ts b/desktop/cmp/form/FormField.ts index 33fe46f846..b3a5dda4b2 100644 --- a/desktop/cmp/form/FormField.ts +++ b/desktop/cmp/form/FormField.ts @@ -352,7 +352,7 @@ function getValidChild(children) { return child; } -function getErrorTooltipContent(errors: string[]): ReactNode { +function getErrorTooltipContent(errors: string[]): ReactElement | string { // If no errors, something other than null must be returned. // If null is returned, as of Blueprint v5, the Blueprint Tooltip component causes deep re-renders of its target // when content changes from null <-> not null. diff --git a/desktop/cmp/grid/editors/EditorProps.ts b/desktop/cmp/grid/editors/EditorProps.ts index 4eb645e02b..7d9da8d3ed 100644 --- a/desktop/cmp/grid/editors/EditorProps.ts +++ b/desktop/cmp/grid/editors/EditorProps.ts @@ -11,7 +11,7 @@ import {HoistProps} from '@xh/hoist/core'; import {StoreRecord} from '@xh/hoist/data'; import '@xh/hoist/desktop/register'; -export interface EditorProps extends HoistProps { +export interface EditorProps> extends HoistProps { /** Column in StoreRecord being edited. */ column: Column; diff --git a/desktop/cmp/grid/editors/impl/InlineEditorModel.ts b/desktop/cmp/grid/editors/impl/InlineEditorModel.ts index 4da3042678..b692cd725c 100644 --- a/desktop/cmp/grid/editors/impl/InlineEditorModel.ts +++ b/desktop/cmp/grid/editors/impl/InlineEditorModel.ts @@ -68,7 +68,7 @@ class InlineEditorModel extends HoistModel { @bindable value; - ref = createObservableRef(); + ref = createObservableRef>(); agParams: CustomCellEditorProps; diff --git a/desktop/cmp/grid/impl/filter/ColumnHeaderFilter.ts b/desktop/cmp/grid/impl/filter/ColumnHeaderFilter.ts index 3514328dc8..fde75fe118 100644 --- a/desktop/cmp/grid/impl/filter/ColumnHeaderFilter.ts +++ b/desktop/cmp/grid/impl/filter/ColumnHeaderFilter.ts @@ -38,7 +38,6 @@ export const columnHeaderFilter = hoistCmp.factory({ ), popoverClassName: 'xh-popup--framed', position: 'right-top', - boundary: 'viewport', hasBackdrop: true, interactionKind: 'click', onInteraction: open => { diff --git a/desktop/cmp/grouping/GroupingChooser.ts b/desktop/cmp/grouping/GroupingChooser.ts index 0aa309de45..67fc349d78 100644 --- a/desktop/cmp/grouping/GroupingChooser.ts +++ b/desktop/cmp/grouping/GroupingChooser.ts @@ -121,7 +121,10 @@ export const [GroupingChooser, groupingChooser] = hoistCmp.withFactory, 'onChange'>, - HoistInputProps { + HoistInputProps { /** * True to allow buttons to be unselected (aka inactivated). Defaults to false. * Does not apply when enableMulti: true. @@ -55,7 +55,7 @@ export const [ButtonGroupInput, buttonGroupInput] = hoistCmp.withFactory { override xhImpl = true; get enableMulti(): boolean { diff --git a/desktop/cmp/input/Checkbox.ts b/desktop/cmp/input/Checkbox.ts index d7f780f644..612569ea03 100644 --- a/desktop/cmp/input/Checkbox.ts +++ b/desktop/cmp/input/Checkbox.ts @@ -5,7 +5,7 @@ * Copyright © 2024 Extremely Heavy Industries Inc. */ import {HoistInputModel, HoistInputProps, useHoistInputModel} from '@xh/hoist/cmp/input'; -import {hoistCmp, HSide, StyleProps} from '@xh/hoist/core'; +import {DefaultHoistProps, hoistCmp, HSide, StyleProps} from '@xh/hoist/core'; import '@xh/hoist/desktop/register'; import {checkbox as bpCheckbox} from '@xh/hoist/kit/blueprint'; import {TEST_ID, withDefault} from '@xh/hoist/utils/js'; @@ -14,7 +14,7 @@ import {ReactNode} from 'react'; import './Checkbox.scss'; -export interface CheckboxProps extends HoistInputProps, StyleProps { +export interface CheckboxProps extends HoistInputProps, StyleProps { value?: boolean; /** True to focus the control on render. */ @@ -54,34 +54,35 @@ export const [Checkbox, checkbox] = hoistCmp.withFactory({ //---------------------------------- // Implementation //---------------------------------- -class CheckboxInputModel extends HoistInputModel { +class CheckboxInputModel extends HoistInputModel { override xhImpl = true; } -const cmp = hoistCmp.factory(({model, className, ...props}, ref) => { - const {renderValue} = model, - labelSide = withDefault(props.labelSide, 'right'), - displayUnsetState = withDefault(props.displayUnsetState, false), - valueIsUnset = isNil(renderValue); +const cmp = hoistCmp.factory>( + ({model, className, ...props}, ref) => { + const {renderValue} = model, + labelSide = withDefault(props.labelSide, 'right'), + displayUnsetState = withDefault(props.displayUnsetState, false), + valueIsUnset = isNil(renderValue); - return bpCheckbox({ - autoFocus: props.autoFocus, - checked: !!renderValue, - indeterminate: valueIsUnset && displayUnsetState, - alignIndicator: labelSide === 'left' ? 'right' : 'left', - disabled: props.disabled, - inline: withDefault(props.inline, true), - label: props.label, - tabIndex: props.tabIndex, - id: props.id, - [TEST_ID]: props.testId, - className, - style: props.style, + return bpCheckbox({ + checked: !!renderValue, + indeterminate: valueIsUnset && displayUnsetState, + alignIndicator: labelSide === 'left' ? 'right' : 'left', + disabled: props.disabled, + inline: withDefault(props.inline, true), + label: props.label, + tabIndex: props.tabIndex, + id: props.id, + [TEST_ID]: props.testId, + className, + style: props.style, - onBlur: model.onBlur, - onFocus: model.onFocus, - onChange: e => model.noteValueChange(e.target.checked), - inputRef: model.inputRef, - ref - }); -}); + onBlur: model.onBlur, + onFocus: model.onFocus, + onChange: e => model.noteValueChange(e.target.checked), + inputRef: model.inputRef, + ref + }); + } +); diff --git a/desktop/cmp/input/CodeInput.ts b/desktop/cmp/input/CodeInput.ts index 6fe9f75cc3..69c72a2348 100644 --- a/desktop/cmp/input/CodeInput.ts +++ b/desktop/cmp/input/CodeInput.ts @@ -47,7 +47,7 @@ import {ReactElement} from 'react'; import {findDOMNode} from 'react-dom'; import './CodeInput.scss'; -export interface CodeInputProps extends HoistInputProps, LayoutProps { +export interface CodeInputProps extends HoistInputProps, LayoutProps { /** True to focus the control on render. */ autoFocus?: boolean; @@ -133,7 +133,7 @@ export const [CodeInput, codeInput] = hoistCmp.withFactory({ //------------------------------ // Implementation //------------------------------ -class CodeInputModel extends HoistInputModel { +class CodeInputModel extends HoistInputModel { override xhImpl = true; @managed @@ -484,7 +484,7 @@ const inputCmp = hoistCmp.factory & B className: 'xh-code-input__inner-wrapper', item: textArea({ value: model.renderValue || '', - ref: model.manageCodeEditor, + inputRef: model.manageCodeEditor, // TODO - confirm this is correct change onChange: model.onChange }) }), diff --git a/desktop/cmp/input/DateInput.ts b/desktop/cmp/input/DateInput.ts index ab8f399103..65906389fb 100644 --- a/desktop/cmp/input/DateInput.ts +++ b/desktop/cmp/input/DateInput.ts @@ -6,6 +6,7 @@ */ import {PopperBoundary, PopperModifierOverrides} from '@blueprintjs/core'; import {TimePickerProps} from '@blueprintjs/datetime'; +import {ReactDayPickerSingleProps} from '@blueprintjs/datetime2/src/common/reactDayPickerProps'; import {HoistInputModel, HoistInputProps, useHoistInputModel} from '@xh/hoist/cmp/input'; import {div} from '@xh/hoist/cmp/layout'; import {WithoutModelAndRef, hoistCmp, HoistProps, HSide, LayoutProps, Some} from '@xh/hoist/core'; @@ -23,15 +24,14 @@ import {getLayoutProps} from '@xh/hoist/utils/react'; import classNames from 'classnames'; import {assign, castArray, clone, trim} from 'lodash'; import moment from 'moment'; -import {createRef, ReactElement, ReactNode} from 'react'; -import {DayPickerProps} from 'react-day-picker'; +import {createRef, JSX, ReactElement} from 'react'; import './DateInput.scss'; -export interface DateInputProps extends LayoutProps, HoistInputProps { +export interface DateInputProps extends LayoutProps, HoistInputProps { value?: Date | LocalDate; /** Props passed to ReactDayPicker component, as per DayPicker docs. */ - dayPickerProps?: DayPickerProps; + dayPickerProps?: ReactDayPickerSingleProps['dayPickerProps']; /** Enable using the DatePicker popover. Default true. */ enablePicker?: boolean; @@ -71,7 +71,7 @@ export interface DateInputProps extends LayoutProps, HoistInputProps { * Element to display inline on the right side of the input. Note if provided, this will * take the place of the (default) calendar-picker button and (optional) clear button. */ - rightElement?: ReactNode; + rightElement?: JSX.Element; /** * Maximum (inclusive) valid date that can be entered by the user via the calendar picker or @@ -170,7 +170,7 @@ export const [DateInput, dateInput] = hoistCmp.withFactory({ //--------------------------------- // Implementation //--------------------------------- -class DateInputModel extends HoistInputModel { +class DateInputModel extends HoistInputModel { override xhImpl = true; @bindable popoverOpen: boolean = false; @@ -440,7 +440,7 @@ const cmp = hoistCmp.factory< enforceFocus: false, modifiers: props.popoverModifiers, position: props.popoverPosition ?? 'auto', - boundary: props.popoverBoundary ?? 'viewport', + boundary: props.popoverBoundary ?? 'clippingParents', portalContainer: props.portalContainer ?? document.body, popoverRef: model.popoverRef, onClose: model.onPopoverClose, diff --git a/desktop/cmp/input/NumberInput.ts b/desktop/cmp/input/NumberInput.ts index a2b65fd4ef..e1bdce10a6 100644 --- a/desktop/cmp/input/NumberInput.ts +++ b/desktop/cmp/input/NumberInput.ts @@ -14,9 +14,12 @@ import {wait} from '@xh/hoist/promise'; import {debounced, TEST_ID, throwIf, withDefault} from '@xh/hoist/utils/js'; import {getLayoutProps} from '@xh/hoist/utils/react'; import {isNaN, isNil, isNumber, round} from 'lodash'; -import {ReactElement, ReactNode, Ref, useLayoutEffect} from 'react'; +import {KeyboardEventHandler, ReactElement, ReactNode, Ref, useLayoutEffect} from 'react'; -export interface NumberInputProps extends LayoutProps, StyleProps, HoistInputProps { +export interface NumberInputProps + extends LayoutProps, + StyleProps, + HoistInputProps { value?: number; /** True to focus the control on render. */ @@ -56,7 +59,7 @@ export interface NumberInputProps extends LayoutProps, StyleProps, HoistInputPro majorStepSize?: number; /** Callback for normalized keydown event. */ - onKeyDown?: (e: KeyboardEvent) => void; + onKeyDown?: KeyboardEventHandler; /** Text to display when control is empty. */ placeholder?: string; @@ -121,7 +124,7 @@ export const [NumberInput, numberInput] = hoistCmp.withFactory //----------------------- // Implementation //----------------------- -class NumberInputModel extends HoistInputModel { +class NumberInputModel extends HoistInputModel { override xhImpl = true; constructor() { @@ -185,7 +188,7 @@ class NumberInputModel extends HoistInputModel { return true; } - onKeyDown = (ev: KeyboardEvent) => { + onKeyDown: KeyboardEventHandler = ev => { if (ev.key === 'Enter') this.doCommit(); this.componentProps.onKeyDown?.(ev); }; @@ -226,7 +229,7 @@ class NumberInputModel extends HoistInputModel { } } -const cmp = hoistCmp.factory(({model, className, ...props}, ref) => { +const cmp = hoistCmp.factory(({model, className, ...props}) => { const {width, flex, ...layoutProps} = getLayoutProps(props), renderValue = model.formatRenderValue(model.renderValue); @@ -278,7 +281,6 @@ const cmp = hoistCmp.factory(({model, className, ...props}, re onBlur: model.onBlur, onFocus: model.onFocus, onKeyDown: model.onKeyDown, - onValueChange: model.onValueChange, - ref + onValueChange: model.onValueChange }); }); diff --git a/desktop/cmp/input/RadioInput.ts b/desktop/cmp/input/RadioInput.ts index f872b1261b..9918e83989 100644 --- a/desktop/cmp/input/RadioInput.ts +++ b/desktop/cmp/input/RadioInput.ts @@ -13,7 +13,7 @@ import {getTestId, TEST_ID, withDefault} from '@xh/hoist/utils/js'; import {filter, isObject} from 'lodash'; import './RadioInput.scss'; -export interface RadioInputProps extends HoistInputProps { +export interface RadioInputProps extends HoistInputProps { /** True to display each radio button inline with each other. */ inline?: boolean; @@ -43,7 +43,7 @@ export const [RadioInput, radioInput] = hoistCmp.withFactory({ //----------------------- // Implementation //----------------------- -class RadioInputModel extends HoistInputModel { +class RadioInputModel extends HoistInputModel { override xhImpl = true; get enabledInputs(): HTMLInputElement[] { @@ -112,7 +112,6 @@ const cmp = hoistCmp.factory(({model, className, ...props}, ref inline: props.inline, selectedValue: model.renderValue, onChange: model.onChange, - testId: props.testId, - ref + [TEST_ID]: props.testId }); }); diff --git a/desktop/cmp/input/Select.ts b/desktop/cmp/input/Select.ts index 8382f7febe..a091e9f12e 100644 --- a/desktop/cmp/input/Select.ts +++ b/desktop/cmp/input/Select.ts @@ -47,7 +47,7 @@ import './Select.scss'; export const MENU_PORTAL_ID = 'xh-select-input-portal'; -export interface SelectProps extends HoistInputProps, LayoutProps { +export interface SelectProps extends HoistInputProps, LayoutProps { /** True to focus the control on render. */ autoFocus?: boolean; @@ -220,7 +220,7 @@ export const [Select, select] = hoistCmp.withFactory({ //----------------------- // Implementation //----------------------- -class SelectInputModel extends HoistInputModel { +class SelectInputModel extends HoistInputModel { override xhImpl = true; // Normalized collection of selectable options. Passed directly to synchronous select. diff --git a/desktop/cmp/input/Slider.ts b/desktop/cmp/input/Slider.ts index 3d127537ba..84974e6e20 100644 --- a/desktop/cmp/input/Slider.ts +++ b/desktop/cmp/input/Slider.ts @@ -4,6 +4,10 @@ * * Copyright © 2024 Extremely Heavy Industries Inc. */ +import { + RangeSliderProps as BpRangeSliderProps, + SliderProps as BpSliderProps +} from '@blueprintjs/core'; import {HoistInputModel, HoistInputProps, useHoistInputModel} from '@xh/hoist/cmp/input'; import {box} from '@xh/hoist/cmp/layout'; import {hoistCmp, LayoutProps, Some} from '@xh/hoist/core'; @@ -15,7 +19,7 @@ import {isArray} from 'lodash'; import {ForwardedRef, ReactNode} from 'react'; import './Slider.scss'; -export interface SliderProps extends HoistInputProps, LayoutProps { +export interface SliderProps extends Omit, 'tabIndex'>, LayoutProps { value?: Some; /** Maximum value */ @@ -62,7 +66,7 @@ export const [Slider, slider] = hoistCmp.withFactory({ //----------------------- // Implementation //----------------------- -class SliderInputModel extends HoistInputModel { +class SliderInputModel extends HoistInputModel { override xhImpl = true; get sliderHandle(): HTMLElement { @@ -79,8 +83,7 @@ class SliderInputModel extends HoistInputModel { } const cmp = hoistCmp.factory(({model, className, ...props}, ref) => { - const {width, ...layoutProps} = getLayoutProps(props), - sliderType = isArray(model.renderValue) ? bpRangeSlider : bpSlider; + const {width, ...layoutProps} = getLayoutProps(props); throwIf(props.labelStepSize <= 0, 'Error in Slider: labelStepSize must be greater than zero.'); @@ -88,22 +91,25 @@ const cmp = hoistCmp.factory(({model, className, ...props}, re if (!layoutProps.padding && !layoutProps.paddingLeft) layoutProps.paddingLeft = 20; if (!layoutProps.padding && !layoutProps.paddingRight) layoutProps.paddingRight = 20; + const sliderProps: BpRangeSliderProps | BpSliderProps = { + value: model.renderValue, + + disabled: props.disabled, + labelRenderer: props.labelRenderer, + labelStepSize: props.labelStepSize, + max: props.max, + min: props.min, + showTrackFill: props.showTrackFill, + stepSize: props.stepSize, + vertical: props.vertical, + + onChange: val => model.noteValueChange(val) + }; + return box({ - item: sliderType({ - value: model.renderValue, - - disabled: props.disabled, - labelRenderer: props.labelRenderer, - labelStepSize: props.labelStepSize, - max: props.max, - min: props.min, - showTrackFill: props.showTrackFill, - stepSize: props.stepSize, - tabIndex: props.tabIndex, - vertical: props.vertical, - - onChange: val => model.noteValueChange(val) - }), + item: isArray(model.renderValue) + ? bpRangeSlider(sliderProps as BpRangeSliderProps) + : bpSlider(sliderProps as BpSliderProps), ...layoutProps, width: withDefault(width, 200), diff --git a/desktop/cmp/input/SwitchInput.ts b/desktop/cmp/input/SwitchInput.ts index 66f7eb13f9..4c608f13a9 100644 --- a/desktop/cmp/input/SwitchInput.ts +++ b/desktop/cmp/input/SwitchInput.ts @@ -5,14 +5,14 @@ * Copyright © 2024 Extremely Heavy Industries Inc. */ import {HoistInputModel, HoistInputProps, useHoistInputModel} from '@xh/hoist/cmp/input'; -import {hoistCmp, HSide, StyleProps} from '@xh/hoist/core'; +import {DefaultHoistProps, hoistCmp, HSide, StyleProps} from '@xh/hoist/core'; import '@xh/hoist/desktop/register'; import {switchControl} from '@xh/hoist/kit/blueprint'; import {TEST_ID, withDefault} from '@xh/hoist/utils/js'; import {ReactNode} from 'react'; import './SwitchInput.scss'; -export interface SwitchInputProps extends HoistInputProps, StyleProps { +export interface SwitchInputProps extends HoistInputProps, StyleProps { value?: boolean; /** True if the control should appear as an inline element (defaults to true). */ @@ -42,31 +42,33 @@ export const [SwitchInput, switchInput] = hoistCmp.withFactory //----------------------- // Implementation //----------------------- -class SwitchInputModel extends HoistInputModel { +class SwitchInputModel extends HoistInputModel { override xhImpl = true; } -const cmp = hoistCmp.factory(({model, className, ...props}, ref) => { - const labelSide = withDefault(props.labelSide, 'right'); +const cmp = hoistCmp.factory>( + ({model, className, ...props}, ref) => { + const labelSide = withDefault(props.labelSide, 'right'); - return switchControl({ - checked: !!model.renderValue, + return switchControl({ + checked: !!model.renderValue, - alignIndicator: labelSide === 'left' ? 'right' : 'left', - disabled: props.disabled, - inline: withDefault(props.inline, true), - label: props.label, - style: props.style, - tabIndex: props.tabIndex, + alignIndicator: labelSide === 'left' ? 'right' : 'left', + disabled: props.disabled, + inline: withDefault(props.inline, true), + label: props.label, + style: props.style, + tabIndex: props.tabIndex, - id: props.id, - className, + id: props.id, + className, - [TEST_ID]: props.testId, - onBlur: model.onBlur, - onFocus: model.onFocus, - onChange: e => model.noteValueChange(e.target.checked), - inputRef: model.inputRef, - ref - }); -}); + [TEST_ID]: props.testId, + onBlur: model.onBlur, + onFocus: model.onFocus, + onChange: e => model.noteValueChange(e.target.checked), + inputRef: model.inputRef, + ref + }); + } +); diff --git a/desktop/cmp/input/TextArea.ts b/desktop/cmp/input/TextArea.ts index e56a4e37fb..1e0898c3db 100644 --- a/desktop/cmp/input/TextArea.ts +++ b/desktop/cmp/input/TextArea.ts @@ -14,7 +14,10 @@ import {getLayoutProps} from '@xh/hoist/utils/react'; import {Ref} from 'react'; import './TextArea.scss'; -export interface TextAreaProps extends HoistInputProps, LayoutProps, StyleProps { +export interface TextAreaProps + extends HoistInputProps, + LayoutProps, + StyleProps { value?: string; /** True to focus the control on render. */ @@ -54,7 +57,7 @@ export const [TextArea, textArea] = hoistCmp.withFactory({ //----------------------- // Implementation //----------------------- -class TextAreaInputModel extends HoistInputModel { +class TextAreaInputModel extends HoistInputModel { override xhImpl = true; override get commitOnChange() { @@ -78,7 +81,7 @@ class TextAreaInputModel extends HoistInputModel { }; } -const cmp = hoistCmp.factory(({model, className, ...rest}, ref) => { +const cmp = hoistCmp.factory(({model, className, ...rest}) => { const props = rest as PlainObject, {width, height, flex, ...layoutProps} = getLayoutProps(props); @@ -104,7 +107,6 @@ const cmp = hoistCmp.factory(({model, className, ...rest}, r onBlur: model.onBlur, onChange: model.onChange, onFocus: model.onFocus, - onKeyDown: model.onKeyDown, - ref + onKeyDown: model.onKeyDown }); }); diff --git a/desktop/cmp/input/TextInput.ts b/desktop/cmp/input/TextInput.ts index f127937fde..7271a170d5 100644 --- a/desktop/cmp/input/TextInput.ts +++ b/desktop/cmp/input/TextInput.ts @@ -22,9 +22,9 @@ import {inputGroup} from '@xh/hoist/kit/blueprint'; import {getTestId, TEST_ID, withDefault} from '@xh/hoist/utils/js'; import {getLayoutProps} from '@xh/hoist/utils/react'; import {isEmpty} from 'lodash'; -import {FocusEvent, ReactElement, ReactNode, Ref} from 'react'; +import {FocusEvent, JSX, KeyboardEventHandler, ReactElement, Ref} from 'react'; -export interface TextInputProps extends HoistInputProps, LayoutProps, StyleProps { +export interface TextInputProps extends HoistInputProps, LayoutProps, StyleProps { value?: string; /** @@ -61,7 +61,7 @@ export interface TextInputProps extends HoistInputProps, LayoutProps, StyleProps placeholder?: string; /** Element to display inline on the right side of the input. */ - rightElement?: ReactNode; + rightElement?: JSX.Element; /** True to display with rounded caps. */ round?: boolean; @@ -94,7 +94,7 @@ export const [TextInput, textInput] = hoistCmp.withFactory({ //----------------------- // Implementation //----------------------- -export class TextInputModel extends HoistInputModel { +export class TextInputModel extends HoistInputModel { override xhImpl = true; override get commitOnChange() { @@ -107,7 +107,7 @@ export class TextInputModel extends HoistInputModel { this.noteValueChange(value); }; - onKeyDown = (ev: KeyboardEvent) => { + onKeyDown: KeyboardEventHandler = ev => { if (ev.key === 'Enter') this.doCommit(); this.componentProps.onKeyDown?.(ev); }; diff --git a/desktop/cmp/tab/TabSwitcher.ts b/desktop/cmp/tab/TabSwitcher.ts index 83dd9ffeca..a81cf50501 100644 --- a/desktop/cmp/tab/TabSwitcher.ts +++ b/desktop/cmp/tab/TabSwitcher.ts @@ -101,7 +101,6 @@ export const [TabSwitcher, tabSwitcher] = hoistCmp.withFactory item: bpTooltip({ content: tooltip, disabled: !tooltip, - boundary: 'viewport', hoverOpenDelay: 1000, position: flipOrientation(orientation), item: hframe({ @@ -142,7 +141,7 @@ export const [TabSwitcher, tabSwitcher] = hoistCmp.withFactory animate, items, selectedTabId: activeTabId, - onChange: tabId => model.activateTab(tabId) + onChange: tabId => model.activateTab(tabId as string) }), onKeyDown: e => impl.onKeyDown(e) }), diff --git a/desktop/cmp/toolbar/Toolbar.ts b/desktop/cmp/toolbar/Toolbar.ts index 0ce53fcb40..8c8b462eb1 100644 --- a/desktop/cmp/toolbar/Toolbar.ts +++ b/desktop/cmp/toolbar/Toolbar.ts @@ -106,6 +106,7 @@ const overflowBox = hoistCmp.factory({ observer: false, memo: false, render({children, minVisibleItems, collapseFrom}) { + // @ts-expect-error - TS doesn't like how we remap items to $items return overflowList({ $items: children, minVisibleItems, diff --git a/kit/react-select/index.ts b/kit/react-select/index.ts index 5d1f5ad5d0..9968f31f5f 100644 --- a/kit/react-select/index.ts +++ b/kit/react-select/index.ts @@ -17,4 +17,5 @@ export const reactSelect = elementFactory(Select), reactCreatableSelect = elementFactory(Creatable), reactAsyncSelect = elementFactory(AsyncSelect), reactAsyncCreatableSelect = elementFactory(AsyncCreatable), - reactWindowedSelect = elementFactory(WindowedSelect); + // Typed as any due to issue with react-windowed-select types + reactWindowedSelect = elementFactory(WindowedSelect); diff --git a/mobile/cmp/input/ButtonGroupInput.ts b/mobile/cmp/input/ButtonGroupInput.ts index 3943dfa70d..57ff8c9061 100644 --- a/mobile/cmp/input/ButtonGroupInput.ts +++ b/mobile/cmp/input/ButtonGroupInput.ts @@ -5,17 +5,17 @@ * Copyright © 2024 Extremely Heavy Industries Inc. */ import {HoistInputModel, HoistInputProps, useHoistInputModel} from '@xh/hoist/cmp/input'; -import {hoistCmp, WithoutModelAndRef, XH} from '@xh/hoist/core'; +import {DefaultHoistProps, hoistCmp, WithoutModelAndRef, XH} from '@xh/hoist/core'; import {Button, buttonGroup, ButtonGroupProps, ButtonProps} from '@xh/hoist/mobile/cmp/button'; import '@xh/hoist/mobile/register'; import {throwIf, warnIf, withDefault} from '@xh/hoist/utils/js'; import {getLayoutProps, getNonLayoutProps} from '@xh/hoist/utils/react'; import {castArray, isEmpty, without} from 'lodash'; -import {Children, cloneElement, ForwardedRef, isValidElement, ReactNode} from 'react'; +import {Children, cloneElement, isValidElement, ReactNode} from 'react'; import './ButtonGroupInput.scss'; export interface ButtonGroupInputProps - extends HoistInputProps, + extends HoistInputProps, WithoutModelAndRef { /** * True to allow buttons to be unselected (aka inactivated). Used when enableMulti is false. @@ -50,7 +50,7 @@ export const [ButtonGroupInput, buttonGroupInput] = hoistCmp.withFactory { override xhImpl = true; get enableMulti() { @@ -86,45 +86,47 @@ class ButtonGroupInputModel extends HoistInputModel { } } -const cmp = hoistCmp.factory(({model, className, ...props}, ref) => { - const { - children, - disabled, - enableClear, - enableMulti, - tabIndex = 0, - ...rest - } = getNonLayoutProps(props); - - const buttons = Children.map(children as ReactNode[], button => { - if (!button) return null; - - if (!isValidElement(button) || button.type !== Button) { - throw XH.exception('ButtonGroupInput child must be a Button.'); - } - - const {value} = button.props, - btnDisabled = disabled || button.props.disabled; - - throwIf(value == null, 'ButtonGroupInput child must declare a non-null value'); - - const isActive = model.isActive(value); - - return cloneElement(button, { - active: isActive, - disabled: withDefault(btnDisabled, false), - onClick: () => model.onButtonClick(value) - } as ButtonProps); - }); - - return buttonGroup({ - items: buttons, - tabIndex, - onBlur: model.onBlur, - onFocus: model.onFocus, - ...rest, - ...getLayoutProps(props), - className, - ref: ref as ForwardedRef - }); -}); +const cmp = hoistCmp.factory>( + ({model, className, ...props}, ref) => { + const { + children, + disabled, + enableClear, + enableMulti, + tabIndex = 0, + ...rest + } = getNonLayoutProps(props); + + const buttons = Children.map(children as ReactNode[], button => { + if (!button) return null; + + if (!isValidElement(button) || button.type !== Button) { + throw XH.exception('ButtonGroupInput child must be a Button.'); + } + + const {value} = button.props, + btnDisabled = disabled || button.props.disabled; + + throwIf(value == null, 'ButtonGroupInput child must declare a non-null value'); + + const isActive = model.isActive(value); + + return cloneElement(button, { + active: isActive, + disabled: withDefault(btnDisabled, false), + onClick: () => model.onButtonClick(value) + } as ButtonProps); + }); + + return buttonGroup({ + items: buttons, + tabIndex, + onBlur: model.onBlur, + onFocus: model.onFocus, + ...rest, + ...getLayoutProps(props), + className, + ref + }); + } +); diff --git a/mobile/cmp/input/Checkbox.ts b/mobile/cmp/input/Checkbox.ts index b26cccf269..ca445a6e13 100644 --- a/mobile/cmp/input/Checkbox.ts +++ b/mobile/cmp/input/Checkbox.ts @@ -10,7 +10,7 @@ import {checkbox as onsenCheckbox} from '@xh/hoist/kit/onsen'; import '@xh/hoist/mobile/register'; import './Checkbox.scss'; -export interface CheckboxProps extends HoistInputProps { +export interface CheckboxProps extends HoistInputProps { value?: boolean; /** Onsen modifier string */ @@ -28,7 +28,7 @@ export const [Checkbox, checkbox] = hoistCmp.withFactory({ } }); -class CheckboxInputModel extends HoistInputModel { +class CheckboxInputModel extends HoistInputModel { override xhImpl = true; } diff --git a/mobile/cmp/input/CheckboxButton.ts b/mobile/cmp/input/CheckboxButton.ts index 34c2cad16e..9af765d6e4 100644 --- a/mobile/cmp/input/CheckboxButton.ts +++ b/mobile/cmp/input/CheckboxButton.ts @@ -12,7 +12,9 @@ import './CheckboxButton.scss'; import {button, ButtonProps} from '@xh/hoist/mobile/cmp/button'; import {withDefault} from '@xh/hoist/utils/js'; -export interface CheckboxButtonProps extends WithoutModelAndRef, HoistInputProps { +export interface CheckboxButtonProps + extends WithoutModelAndRef, + HoistInputProps { value?: boolean; } @@ -29,7 +31,7 @@ export const [CheckboxButton, checkboxButton] = hoistCmp.withFactory { override xhImpl = true; } diff --git a/mobile/cmp/input/DateInput.ts b/mobile/cmp/input/DateInput.ts index d35a565340..00a9e6837e 100644 --- a/mobile/cmp/input/DateInput.ts +++ b/mobile/cmp/input/DateInput.ts @@ -19,7 +19,7 @@ import moment from 'moment'; import './DateInput.scss'; import {ForwardedRef, ReactElement} from 'react'; -export interface DateInputProps extends HoistInputProps, StyleProps, LayoutProps { +export interface DateInputProps extends HoistInputProps, StyleProps, LayoutProps { value?: Date | LocalDate; /** True to show a "clear" button aligned to the right of the control. Default false. */ @@ -90,7 +90,7 @@ export const [DateInput, dateInput] = hoistCmp.withFactory({ //--------------------------------- // Implementation //--------------------------------- -class DateInputModel extends HoistInputModel { +class DateInputModel extends HoistInputModel { override xhImpl = true; @observable popoverOpen = false; diff --git a/mobile/cmp/input/Label.ts b/mobile/cmp/input/Label.ts index e5331ba729..53d339a131 100644 --- a/mobile/cmp/input/Label.ts +++ b/mobile/cmp/input/Label.ts @@ -6,12 +6,11 @@ */ import {HoistInputModel, HoistInputProps, useHoistInputModel} from '@xh/hoist/cmp/input'; import {div} from '@xh/hoist/cmp/layout'; -import {hoistCmp, StyleProps} from '@xh/hoist/core'; +import {DefaultHoistProps, hoistCmp, StyleProps} from '@xh/hoist/core'; import '@xh/hoist/mobile/register'; import './Label.scss'; -import {ForwardedRef} from 'react'; -export interface LabelProps extends HoistInputProps, StyleProps {} +export interface LabelProps extends HoistInputProps, StyleProps {} /** * A simple label for a form. @@ -24,18 +23,20 @@ export const [Label, label] = hoistCmp.withFactory({ } }); -class LabelInputModel extends HoistInputModel { +class LabelInputModel extends HoistInputModel { override xhImpl = true; } //----------------------- // Implementation //----------------------- -const cmp = hoistCmp.factory(({model, className, style, width, children}, ref) => { - return div({ - className, - style: {...style, whiteSpace: 'nowrap', width}, - items: children, - ref: ref as ForwardedRef - }); -}); +const cmp = hoistCmp.factory>( + ({className, style, width, children}, ref) => { + return div({ + className, + style: {...style, whiteSpace: 'nowrap', width}, + items: children, + ref + }); + } +); diff --git a/mobile/cmp/input/NumberInput.ts b/mobile/cmp/input/NumberInput.ts index 6d3ce02f7e..18e90824cf 100644 --- a/mobile/cmp/input/NumberInput.ts +++ b/mobile/cmp/input/NumberInput.ts @@ -15,7 +15,7 @@ import {getLayoutProps} from '@xh/hoist/utils/react'; import {isNaN, isNil, isNumber, round} from 'lodash'; import './NumberInput.scss'; -export interface NumberInputProps extends HoistInputProps, StyleProps, LayoutProps { +export interface NumberInputProps extends HoistInputProps, StyleProps, LayoutProps { value?: number; /** True to commit on every change/keystroke, default false. */ @@ -87,7 +87,7 @@ export const [NumberInput, numberInput] = hoistCmp.withFactory //----------------------- // Implementation //----------------------- -class NumberInputModel extends HoistInputModel { +class NumberInputModel extends HoistInputModel { override xhImpl = true; static shorthandValidator = /((\.\d+)|(\d+(\.\d+)?))([kmb])\b/i; diff --git a/mobile/cmp/input/SearchInput.ts b/mobile/cmp/input/SearchInput.ts index 0154fd40a8..d84ee80f40 100644 --- a/mobile/cmp/input/SearchInput.ts +++ b/mobile/cmp/input/SearchInput.ts @@ -12,7 +12,7 @@ import {withDefault} from '@xh/hoist/utils/js'; import {getLayoutProps} from '@xh/hoist/utils/react'; import './SearchInput.scss'; -export interface SearchInputProps extends HoistInputProps { +export interface SearchInputProps extends HoistInputProps { value?: string; /** True to commit on every change/keystroke, default false. */ @@ -49,7 +49,7 @@ export const [SearchInput, searchInput] = hoistCmp.withFactory //----------------------- // Implementation //----------------------- -class SearchInputModel extends HoistInputModel { +class SearchInputModel extends HoistInputModel { override xhImpl = true; override get commitOnChange() { diff --git a/mobile/cmp/input/Select.ts b/mobile/cmp/input/Select.ts index 7f9e6ccfa9..2a861cbbcf 100644 --- a/mobile/cmp/input/Select.ts +++ b/mobile/cmp/input/Select.ts @@ -27,7 +27,7 @@ import {Children, ForwardedRef, ReactNode, ReactPortal} from 'react'; import ReactDom from 'react-dom'; import './Select.scss'; -export interface SelectProps extends HoistInputProps, LayoutProps { +export interface SelectProps extends HoistInputProps, LayoutProps { /** * Function to return a "create a new option" string prompt. Requires `allowCreate` true. * Passed current query input. @@ -186,7 +186,7 @@ export const [Select, select] = hoistCmp.withFactory({ //----------------------- // Implementation //----------------------- -class SelectInputModel extends HoistInputModel { +class SelectInputModel extends HoistInputModel { override xhImpl = true; // Normalized collection of selectable options. Passed directly to synchronous select. diff --git a/mobile/cmp/input/SwitchInput.ts b/mobile/cmp/input/SwitchInput.ts index 3c4b4687f5..dba15a4e0b 100644 --- a/mobile/cmp/input/SwitchInput.ts +++ b/mobile/cmp/input/SwitchInput.ts @@ -10,7 +10,7 @@ import {switchControl} from '@xh/hoist/kit/onsen'; import '@xh/hoist/mobile/register'; import './SwitchInput.scss'; -export interface SwitchInputProps extends HoistInputProps, StyleProps { +export interface SwitchInputProps extends HoistInputProps, StyleProps { value?: string; /** Onsen modifier string */ @@ -28,7 +28,7 @@ export const [SwitchInput, switchInput] = hoistCmp.withFactory } }); -class SwitchInputModel extends HoistInputModel { +class SwitchInputModel extends HoistInputModel { override xhImpl = true; } diff --git a/mobile/cmp/input/TextArea.ts b/mobile/cmp/input/TextArea.ts index 442ae2eff3..16ddeb0fef 100644 --- a/mobile/cmp/input/TextArea.ts +++ b/mobile/cmp/input/TextArea.ts @@ -13,7 +13,7 @@ import {getLayoutProps} from '@xh/hoist/utils/react'; import './TextArea.scss'; import {ForwardedRef} from 'react'; -export interface TextAreaProps extends HoistInputProps, StyleProps, LayoutProps { +export interface TextAreaProps extends HoistInputProps, StyleProps, LayoutProps { value?: string; /** True to commit on every change/keystroke, default false. */ @@ -47,7 +47,7 @@ export const [TextArea, textArea] = hoistCmp.withFactory({ //----------------------- // Implementation //----------------------- -class TextAreaInputModel extends HoistInputModel { +class TextAreaInputModel extends HoistInputModel { override xhImpl = true; override get commitOnChange() { diff --git a/mobile/cmp/input/TextInput.ts b/mobile/cmp/input/TextInput.ts index 9a307994b6..9e61a8650f 100644 --- a/mobile/cmp/input/TextInput.ts +++ b/mobile/cmp/input/TextInput.ts @@ -17,7 +17,7 @@ import {isEmpty} from 'lodash'; import './TextInput.scss'; import {ForwardedRef} from 'react'; -export interface TextInputProps extends HoistInputProps, StyleProps, LayoutProps { +export interface TextInputProps extends HoistInputProps, StyleProps, LayoutProps { value?: string; /** @@ -80,7 +80,7 @@ export const [TextInput, textInput] = hoistCmp.withFactory({ //----------------------- // Implementation //----------------------- -class TextInputModel extends HoistInputModel { +class TextInputModel extends HoistInputModel { override xhImpl = true; override get commitOnChange() { From c7ab0db0a18d55b81461e2d703ac93c4f310c001 Mon Sep 17 00:00:00 2001 From: Greg Solomon Date: Fri, 28 Jun 2024 09:36:26 -0400 Subject: [PATCH 17/23] Fix NumberInput forwardRef bug --- desktop/cmp/input/NumberInput.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/desktop/cmp/input/NumberInput.ts b/desktop/cmp/input/NumberInput.ts index e1bdce10a6..b59832a631 100644 --- a/desktop/cmp/input/NumberInput.ts +++ b/desktop/cmp/input/NumberInput.ts @@ -229,7 +229,10 @@ class NumberInputModel extends HoistInputModel { } } -const cmp = hoistCmp.factory(({model, className, ...props}) => { +// Note: we don't use the `ref` here, but the presence of a second argument is required +// for the component to be wrapped with React.forwardRef, which is necessary since +// `useHoistInputModel` always passes a ref to the component, even if it's not used. +const cmp = hoistCmp.factory(({model, className, ...props}, ref) => { const {width, flex, ...layoutProps} = getLayoutProps(props), renderValue = model.formatRenderValue(model.renderValue); From aa18871aa647a49dc44d4c07b3e19a0c1a20454c Mon Sep 17 00:00:00 2001 From: Greg Solomon Date: Fri, 28 Jun 2024 11:07:12 -0400 Subject: [PATCH 18/23] Restore checkbox autoFocus functionality --- desktop/cmp/input/Checkbox.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/desktop/cmp/input/Checkbox.ts b/desktop/cmp/input/Checkbox.ts index 612569ea03..af57741e82 100644 --- a/desktop/cmp/input/Checkbox.ts +++ b/desktop/cmp/input/Checkbox.ts @@ -66,6 +66,7 @@ const cmp = hoistCmp.factory Date: Fri, 28 Jun 2024 11:32:29 -0400 Subject: [PATCH 19/23] Minor cleanups / fixes --- core/AppSpec.ts | 6 +- desktop/cmp/input/ButtonGroupInput.ts | 136 ++++++++-------- desktop/cmp/input/RadioInput.ts | 3 + desktop/cmp/input/Select.ts | 219 +++++++++++++------------- desktop/cmp/input/Slider.ts | 85 +++++----- desktop/cmp/input/TextArea.ts | 5 +- mobile/cmp/grid/impl/ColChooser.ts | 18 ++- mobile/cmp/input/DateInput.ts | 93 ++++++----- mobile/cmp/input/Select.ts | 176 +++++++++++---------- mobile/cmp/input/TextArea.ts | 61 +++---- mobile/cmp/input/TextInput.ts | 81 +++++----- 11 files changed, 460 insertions(+), 423 deletions(-) diff --git a/core/AppSpec.ts b/core/AppSpec.ts index b4bedc5c05..f70edc5464 100644 --- a/core/AppSpec.ts +++ b/core/AppSpec.ts @@ -7,7 +7,7 @@ import {XH, HoistAppModel, ElementFactory, HoistProps, HoistPropsWithModel} from '@xh/hoist/core'; import {throwIf} from '@xh/hoist/utils/js'; import {isFunction, isNil, isString} from 'lodash'; -import {Component, ComponentClass, FunctionComponent} from 'react'; +import {Component, ComponentType, FunctionComponent} from 'react'; /** * Specification for a client-side Hoist application. @@ -40,14 +40,14 @@ export class AppSpec { * Root HoistComponent for the application. Despite the name, * functional components are fully supported and expected. */ - componentClass: ComponentClass> | FunctionComponent>; + componentClass: ComponentType>; /** * Container component to be used to host this application. * This class determines the platform used by Hoist. The value should be imported from * either `@xh/hoist/desktop/AppContainer` or `@xh/hoist/mobile/AppContainer`. */ - containerClass: ComponentClass | FunctionComponent; + containerClass: ComponentType; /** True if the app should use the Hoist mobile toolkit.*/ isMobileApp: boolean; diff --git a/desktop/cmp/input/ButtonGroupInput.ts b/desktop/cmp/input/ButtonGroupInput.ts index 401f5a1b39..710edcafac 100644 --- a/desktop/cmp/input/ButtonGroupInput.ts +++ b/desktop/cmp/input/ButtonGroupInput.ts @@ -5,13 +5,13 @@ * Copyright © 2024 Extremely Heavy Industries Inc. */ import {HoistInputModel, HoistInputProps, useHoistInputModel} from '@xh/hoist/cmp/input'; -import {hoistCmp, Intent, WithoutModelAndRef, XH} from '@xh/hoist/core'; +import {DefaultHoistProps, hoistCmp, Intent, WithoutModelAndRef, XH} from '@xh/hoist/core'; import {Button, buttonGroup, ButtonGroupProps, ButtonProps} from '@xh/hoist/desktop/cmp/button'; import '@xh/hoist/desktop/register'; import {throwIf, warnIf, withDefault} from '@xh/hoist/utils/js'; import {getLayoutProps, getNonLayoutProps} from '@xh/hoist/utils/react'; import {castArray, filter, isEmpty, without} from 'lodash'; -import {Children, cloneElement, ForwardedRef, isValidElement} from 'react'; +import {Children, cloneElement, isValidElement} from 'react'; export interface ButtonGroupInputProps extends Omit, 'onChange'>, @@ -100,68 +100,70 @@ class ButtonGroupInputModel extends HoistInputModel { } } -const cmp = hoistCmp.factory(({model, className, ...props}, ref) => { - const { - children, - // HoistInput Props - bind, - disabled, - onChange, - onCommit, - tabIndex, - value, - // FormField Props - commitOnChange, - // ButtonGroupInput Props - enableClear, - enableMulti, - // Button props applied to each child button - intent, - minimal, - outlined, - // ...and ButtonGroup gets all the rest - ...buttonGroupProps - } = getNonLayoutProps(props); - - const buttons = Children.map(children, button => { - if (!button) return null; - - if (!isValidElement(button) || button.type !== Button) { - throw XH.exception('ButtonGroupInput child must be a Button.'); - } - - const props = button.props as ButtonProps, - {value, intent: btnIntent} = props, - btnDisabled = disabled || props.disabled; - - throwIf( - (enableClear || enableMulti) && value == null, - 'ButtonGroupInput child must declare a non-null value when enableClear or enableMulti are true' - ); - - const isActive = model.isActive(value); - - return cloneElement(button, { - active: isActive, - intent: btnIntent ?? intent, - minimal: withDefault(minimal, false), - outlined: withDefault(outlined, false), - disabled: withDefault(btnDisabled, false), - onClick: () => model.onButtonClick(value), - // Workaround for https://github.com/palantir/blueprint/issues/3971 - key: `${isActive} ${value}`, - autoFocus: isActive && model.hasFocus - } as ButtonGroupProps); - }); - - return buttonGroup({ - items: buttons, - ...(buttonGroupProps as ButtonGroupProps), - minimal: withDefault(minimal, outlined, false), - ...getLayoutProps(props), - onBlur: model.onBlur, - onFocus: model.onFocus, - className, - ref: ref as ForwardedRef - }); -}); +const cmp = hoistCmp.factory>( + ({model, className, ...props}, ref) => { + const { + children, + // HoistInput Props + bind, + disabled, + onChange, + onCommit, + tabIndex, + value, + // FormField Props + commitOnChange, + // ButtonGroupInput Props + enableClear, + enableMulti, + // Button props applied to each child button + intent, + minimal, + outlined, + // ...and ButtonGroup gets all the rest + ...buttonGroupProps + } = getNonLayoutProps(props); + + const buttons = Children.map(children, button => { + if (!button) return null; + + if (!isValidElement(button) || button.type !== Button) { + throw XH.exception('ButtonGroupInput child must be a Button.'); + } + + const props = button.props as ButtonProps, + {value, intent: btnIntent} = props, + btnDisabled = disabled || props.disabled; + + throwIf( + (enableClear || enableMulti) && value == null, + 'ButtonGroupInput child must declare a non-null value when enableClear or enableMulti are true' + ); + + const isActive = model.isActive(value); + + return cloneElement(button, { + active: isActive, + intent: btnIntent ?? intent, + minimal: withDefault(minimal, false), + outlined: withDefault(outlined, false), + disabled: withDefault(btnDisabled, false), + onClick: () => model.onButtonClick(value), + // Workaround for https://github.com/palantir/blueprint/issues/3971 + key: `${isActive} ${value}`, + autoFocus: isActive && model.hasFocus + } as ButtonGroupProps); + }); + + return buttonGroup({ + items: buttons, + ...(buttonGroupProps as ButtonGroupProps), + minimal: withDefault(minimal, outlined, false), + ...getLayoutProps(props), + onBlur: model.onBlur, + onFocus: model.onFocus, + className, + ref + }); + } +); diff --git a/desktop/cmp/input/RadioInput.ts b/desktop/cmp/input/RadioInput.ts index 9918e83989..f1149dc61b 100644 --- a/desktop/cmp/input/RadioInput.ts +++ b/desktop/cmp/input/RadioInput.ts @@ -88,6 +88,9 @@ class RadioInputModel extends HoistInputModel { } } +// Note: we don't use the `ref` here, but the presence of a second argument is required +// for the component to be wrapped with React.forwardRef, which is necessary since +// `useHoistInputModel` always passes a ref to the component, even if it's not used. const cmp = hoistCmp.factory(({model, className, ...props}, ref) => { const {normalizedOptions} = model, labelSide = withDefault(props.labelSide, 'right'); diff --git a/desktop/cmp/input/Select.ts b/desktop/cmp/input/Select.ts index a091e9f12e..b037490bd0 100644 --- a/desktop/cmp/input/Select.ts +++ b/desktop/cmp/input/Select.ts @@ -9,6 +9,7 @@ import {box, div, fragment, hbox, span} from '@xh/hoist/cmp/layout'; import { Awaitable, createElement, + DefaultHoistProps, hoistCmp, LayoutProps, PlainObject, @@ -41,7 +42,7 @@ import { keyBy, merge } from 'lodash'; -import {ForwardedRef, ReactElement, ReactNode} from 'react'; +import {ReactElement, ReactNode} from 'react'; import {components} from 'react-select'; import './Select.scss'; @@ -691,116 +692,118 @@ class SelectInputModel extends HoistInputModel { } } -const cmp = hoistCmp.factory(({model, className, ...props}, ref) => { - const {width, height, ...layoutProps} = getLayoutProps(props), - rsProps: PlainObject = { - value: model.renderValue, - - autoFocus: props.autoFocus, - formatOptionLabel: model.formatOptionLabel, - isDisabled: props.disabled, - isMulti: props.enableMulti, - closeMenuOnSelect: props.closeMenuOnSelect, - hideSelectedOptions: model.hideSelectedOptions, - maxMenuHeight: props.maxMenuHeight, - - // Explicit false ensures consistent default for single and multi-value instances. - isClearable: withDefault(props.enableClear, false), - menuPlacement: withDefault(props.menuPlacement, 'auto'), - noOptionsMessage: model.noOptionsMessageFn, - openMenuOnFocus: props.openMenuOnFocus, - placeholder: withDefault(props.placeholder, 'Select...'), - tabIndex: props.tabIndex, - - // Minimize (or hide) bulky dropdown - components: { - DropdownIndicator: model.getDropdownIndicatorCmp(), - ClearIndicator: model.getClearIndicatorCmp(), - Menu: model.getMenuCmp(), - IndicatorSeparator: () => null, - ValueContainer: model.getValueContainerCmp(), - MultiValueLabel: model.getMultiValueLabelCmp(), - SingleValue: model.getSingleValueCmp() - }, - - // A shared div is created lazily here as needed, appended to the body, and assigned - // a high z-index to ensure options menus render over dialogs or other modals. - menuPortalTarget: model.getOrCreatePortalDiv(), - - inputId: props.id, - classNamePrefix: 'xh-select', - theme: model.getThemeConfig(), - - onBlur: model.onBlur, - onChange: model.onSelectChange, - onFocus: model.onFocus, - filterOption: model.filterOption, +const cmp = hoistCmp.factory>( + ({model, className, ...props}, ref) => { + const {width, height, ...layoutProps} = getLayoutProps(props), + rsProps: PlainObject = { + value: model.renderValue, + + autoFocus: props.autoFocus, + formatOptionLabel: model.formatOptionLabel, + isDisabled: props.disabled, + isMulti: props.enableMulti, + closeMenuOnSelect: props.closeMenuOnSelect, + hideSelectedOptions: model.hideSelectedOptions, + maxMenuHeight: props.maxMenuHeight, + + // Explicit false ensures consistent default for single and multi-value instances. + isClearable: withDefault(props.enableClear, false), + menuPlacement: withDefault(props.menuPlacement, 'auto'), + noOptionsMessage: model.noOptionsMessageFn, + openMenuOnFocus: props.openMenuOnFocus, + placeholder: withDefault(props.placeholder, 'Select...'), + tabIndex: props.tabIndex, + + // Minimize (or hide) bulky dropdown + components: { + DropdownIndicator: model.getDropdownIndicatorCmp(), + ClearIndicator: model.getClearIndicatorCmp(), + Menu: model.getMenuCmp(), + IndicatorSeparator: () => null, + ValueContainer: model.getValueContainerCmp(), + MultiValueLabel: model.getMultiValueLabelCmp(), + SingleValue: model.getSingleValueCmp() + }, + + // A shared div is created lazily here as needed, appended to the body, and assigned + // a high z-index to ensure options menus render over dialogs or other modals. + menuPortalTarget: model.getOrCreatePortalDiv(), + + inputId: props.id, + classNamePrefix: 'xh-select', + theme: model.getThemeConfig(), + + onBlur: model.onBlur, + onChange: model.onSelectChange, + onFocus: model.onFocus, + filterOption: model.filterOption, + + ref: model.reactSelectRef + }; - ref: model.reactSelectRef - }; + if (model.manageInputValue) { + rsProps.inputValue = model.inputValue || ''; + rsProps.onInputChange = model.onInputChange; + rsProps.controlShouldRenderValue = !model.hasFocus; + rsProps.onMenuOpen = () => { + wait().then(() => { + const selectedEl = document.getElementsByClassName( + 'xh-select__option--is-selected' + )[0]; + selectedEl?.scrollIntoView({block: 'end'}); + }); + }; + } - if (model.manageInputValue) { - rsProps.inputValue = model.inputValue || ''; - rsProps.onInputChange = model.onInputChange; - rsProps.controlShouldRenderValue = !model.hasFocus; - rsProps.onMenuOpen = () => { - wait().then(() => { - const selectedEl = document.getElementsByClassName( - 'xh-select__option--is-selected' - )[0]; - selectedEl?.scrollIntoView({block: 'end'}); - }); - }; - } + if (model.asyncMode) { + rsProps.loadOptions = model.doQueryAsync; + rsProps.loadingMessage = model.loadingMessageFn; + if (model.renderValue) rsProps.defaultOptions = [model.renderValue]; + } else { + rsProps.options = model.internalOptions; + rsProps.isSearchable = model.filterMode; + } - if (model.asyncMode) { - rsProps.loadOptions = model.doQueryAsync; - rsProps.loadingMessage = model.loadingMessageFn; - if (model.renderValue) rsProps.defaultOptions = [model.renderValue]; - } else { - rsProps.options = model.internalOptions; - rsProps.isSearchable = model.filterMode; - } + if (model.creatableMode) { + rsProps.formatCreateLabel = model.createMessageFn; + } - if (model.creatableMode) { - rsProps.formatCreateLabel = model.createMessageFn; - } + if (props.menuWidth) { + rsProps.styles = { + menu: provided => ({...provided, width: `${props.menuWidth}px`}), + ...props.rsOptions?.styles + }; + } - if (props.menuWidth) { - rsProps.styles = { - menu: provided => ({...provided, width: `${props.menuWidth}px`}), - ...props.rsOptions?.styles - }; + const factory = model.getSelectFactory(); + merge(rsProps, props.rsOptions); + + return box({ + item: factory(rsProps), + className: classNames(className, height ? 'xh-select--has-height' : null), + onKeyDown: e => { + // Esc. and Enter can be listened for by parents -- stop the keydown event + // propagation only if react-select already likely to have used for menu management. + // note: menuIsOpen will be undefined on AsyncSelect due to a react-select bug. + const menuIsOpen = model.reactSelect?.state?.menuIsOpen; + if (menuIsOpen && (e.key === 'Escape' || e.key === 'Enter')) { + e.stopPropagation(); + } + }, + onMouseDown: e => { + // Some internal elements, like the dropdown indicator and the rendered single value, + // fire 'mousedown' events. These can bubble and inadvertently close Popovers that + // contain Selects. + const target = e?.target as HTMLElement; + if (target && elemWithin(target, 'bp5-popover')) { + e.stopPropagation(); + } + }, + testId: props.testId, + ...layoutProps, + width: withDefault(width, 200), + height: height, + ref + }); } - - const factory = model.getSelectFactory(); - merge(rsProps, props.rsOptions); - - return box({ - item: factory(rsProps), - className: classNames(className, height ? 'xh-select--has-height' : null), - onKeyDown: e => { - // Esc. and Enter can be listened for by parents -- stop the keydown event - // propagation only if react-select already likely to have used for menu management. - // note: menuIsOpen will be undefined on AsyncSelect due to a react-select bug. - const menuIsOpen = model.reactSelect?.state?.menuIsOpen; - if (menuIsOpen && (e.key === 'Escape' || e.key === 'Enter')) { - e.stopPropagation(); - } - }, - onMouseDown: e => { - // Some internal elements, like the dropdown indicator and the rendered single value, - // fire 'mousedown' events. These can bubble and inadvertently close Popovers that - // contain Selects. - const target = e?.target as HTMLElement; - if (target && elemWithin(target, 'bp5-popover')) { - e.stopPropagation(); - } - }, - testId: props.testId, - ...layoutProps, - width: withDefault(width, 200), - height: height, - ref: ref as ForwardedRef - }); -}); +); diff --git a/desktop/cmp/input/Slider.ts b/desktop/cmp/input/Slider.ts index 84974e6e20..e6b72cc4ff 100644 --- a/desktop/cmp/input/Slider.ts +++ b/desktop/cmp/input/Slider.ts @@ -10,13 +10,13 @@ import { } from '@blueprintjs/core'; import {HoistInputModel, HoistInputProps, useHoistInputModel} from '@xh/hoist/cmp/input'; import {box} from '@xh/hoist/cmp/layout'; -import {hoistCmp, LayoutProps, Some} from '@xh/hoist/core'; +import {DefaultHoistProps, hoistCmp, LayoutProps, Some} from '@xh/hoist/core'; import '@xh/hoist/desktop/register'; import {rangeSlider as bpRangeSlider, slider as bpSlider} from '@xh/hoist/kit/blueprint'; import {throwIf, withDefault} from '@xh/hoist/utils/js'; import {getLayoutProps} from '@xh/hoist/utils/react'; import {isArray} from 'lodash'; -import {ForwardedRef, ReactNode} from 'react'; +import {ReactNode} from 'react'; import './Slider.scss'; export interface SliderProps extends Omit, 'tabIndex'>, LayoutProps { @@ -82,41 +82,46 @@ class SliderInputModel extends HoistInputModel { } } -const cmp = hoistCmp.factory(({model, className, ...props}, ref) => { - const {width, ...layoutProps} = getLayoutProps(props); - - throwIf(props.labelStepSize <= 0, 'Error in Slider: labelStepSize must be greater than zero.'); - - // Set default left / right padding - if (!layoutProps.padding && !layoutProps.paddingLeft) layoutProps.paddingLeft = 20; - if (!layoutProps.padding && !layoutProps.paddingRight) layoutProps.paddingRight = 20; - - const sliderProps: BpRangeSliderProps | BpSliderProps = { - value: model.renderValue, - - disabled: props.disabled, - labelRenderer: props.labelRenderer, - labelStepSize: props.labelStepSize, - max: props.max, - min: props.min, - showTrackFill: props.showTrackFill, - stepSize: props.stepSize, - vertical: props.vertical, - - onChange: val => model.noteValueChange(val) - }; - - return box({ - item: isArray(model.renderValue) - ? bpRangeSlider(sliderProps as BpRangeSliderProps) - : bpSlider(sliderProps as BpSliderProps), - - ...layoutProps, - width: withDefault(width, 200), - className, - - onBlur: model.onBlur, - onFocus: model.onFocus, - ref: ref as ForwardedRef - }); -}); +const cmp = hoistCmp.factory>( + ({model, className, ...props}, ref) => { + const {width, ...layoutProps} = getLayoutProps(props); + + throwIf( + props.labelStepSize <= 0, + 'Error in Slider: labelStepSize must be greater than zero.' + ); + + // Set default left / right padding + if (!layoutProps.padding && !layoutProps.paddingLeft) layoutProps.paddingLeft = 20; + if (!layoutProps.padding && !layoutProps.paddingRight) layoutProps.paddingRight = 20; + + const sliderProps: BpRangeSliderProps | BpSliderProps = { + value: model.renderValue, + + disabled: props.disabled, + labelRenderer: props.labelRenderer, + labelStepSize: props.labelStepSize, + max: props.max, + min: props.min, + showTrackFill: props.showTrackFill, + stepSize: props.stepSize, + vertical: props.vertical, + + onChange: val => model.noteValueChange(val) + }; + + return box({ + item: isArray(model.renderValue) + ? bpRangeSlider(sliderProps as BpRangeSliderProps) + : bpSlider(sliderProps as BpSliderProps), + + ...layoutProps, + width: withDefault(width, 200), + className, + + onBlur: model.onBlur, + onFocus: model.onFocus, + ref + }); + } +); diff --git a/desktop/cmp/input/TextArea.ts b/desktop/cmp/input/TextArea.ts index 1e0898c3db..3d41848a80 100644 --- a/desktop/cmp/input/TextArea.ts +++ b/desktop/cmp/input/TextArea.ts @@ -81,7 +81,10 @@ class TextAreaInputModel extends HoistInputModel { }; } -const cmp = hoistCmp.factory(({model, className, ...rest}) => { +// Note: we don't use the `ref` here, but the presence of a second argument is required +// for the component to be wrapped with React.forwardRef, which is necessary since +// `useHoistInputModel` always passes a ref to the component, even if it's not used. +const cmp = hoistCmp.factory(({model, className, ...rest}, ref) => { const props = rest as PlainObject, {width, height, flex, ...layoutProps} = getLayoutProps(props); diff --git a/mobile/cmp/grid/impl/ColChooser.ts b/mobile/cmp/grid/impl/ColChooser.ts index 72d50339ee..9d826ec667 100644 --- a/mobile/cmp/grid/impl/ColChooser.ts +++ b/mobile/cmp/grid/impl/ColChooser.ts @@ -6,7 +6,15 @@ */ import {Column} from '@xh/hoist/cmp/grid'; import {div, filler, placeholder as placeholderCmp} from '@xh/hoist/cmp/layout'; -import {hoistCmp, HoistModel, HoistProps, lookup, useLocalModel, uses} from '@xh/hoist/core'; +import { + DefaultHoistProps, + hoistCmp, + HoistModel, + HoistProps, + lookup, + useLocalModel, + uses +} from '@xh/hoist/core'; import {Icon} from '@xh/hoist/icon'; import {dragDropContext, draggable, droppable} from '@xh/hoist/kit/react-beautiful-dnd'; import {button} from '@xh/hoist/mobile/cmp/button'; @@ -16,7 +24,7 @@ import '@xh/hoist/mobile/register'; import classNames from 'classnames'; import './ColChooser.scss'; import {isEmpty} from 'lodash'; -import {ForwardedRef, ReactNode} from 'react'; +import {ReactNode} from 'react'; import {ColChooserModel} from './ColChooserModel'; export interface ColChooserProps extends HoistProps {} @@ -148,7 +156,7 @@ const columnList = hoistCmp.factory({ ? placeholderCmp('All columns have been added to the grid.') : [...cols.map((col, idx) => draggableRow({col, idx})), placeholder], ...props, - ref: ref + ref }); } }); @@ -175,7 +183,7 @@ const draggableRow = hoistCmp.factory({ } }); -const row = hoistCmp.factory({ +const row = hoistCmp.factory>({ render({model, col, isDragging, ...props}, ref) { if (!col) return null; @@ -215,7 +223,7 @@ const row = hoistCmp.factory({ }) ], ...props, - ref: ref as ForwardedRef + ref }); } }); diff --git a/mobile/cmp/input/DateInput.ts b/mobile/cmp/input/DateInput.ts index 00a9e6837e..2075faac40 100644 --- a/mobile/cmp/input/DateInput.ts +++ b/mobile/cmp/input/DateInput.ts @@ -6,7 +6,14 @@ */ import {HoistInputModel, HoistInputProps, useHoistInputModel} from '@xh/hoist/cmp/input'; import {div} from '@xh/hoist/cmp/layout'; -import {hoistCmp, StyleProps, LayoutProps, HSide, PlainObject} from '@xh/hoist/core'; +import { + hoistCmp, + StyleProps, + LayoutProps, + HSide, + PlainObject, + DefaultHoistProps +} from '@xh/hoist/core'; import {fmtDate} from '@xh/hoist/format'; import {Icon} from '@xh/hoist/icon'; import {singleDatePicker} from '@xh/hoist/kit/react-dates'; @@ -17,7 +24,7 @@ import {withDefault} from '@xh/hoist/utils/js'; import {getLayoutProps} from '@xh/hoist/utils/react'; import moment from 'moment'; import './DateInput.scss'; -import {ForwardedRef, ReactElement} from 'react'; +import {ReactElement} from 'react'; export interface DateInputProps extends HoistInputProps, StyleProps, LayoutProps { value?: Date | LocalDate; @@ -180,43 +187,45 @@ class DateInputModel extends HoistInputModel { } } -const cmp = hoistCmp.factory(({model, className, ...props}, ref) => { - const layoutProps = getLayoutProps(props), - {renderValue} = model, - value = renderValue ? moment(renderValue) : null, - enableClear = withDefault(props.enableClear, false), - textAlign = withDefault(props.textAlign, 'left'), - leftIcon = withDefault(props.leftIcon, null), - rightIcon = withDefault(props.rightIcon, Icon.calendar()), - isOpen = model.popoverOpen && !props.disabled; - - return div({ - className, - items: [ - leftIcon, - singleDatePicker({ - date: value, - focused: isOpen, - onFocusChange: ({focused}) => model.setPopoverOpen(focused), - onDateChange: date => model.onDateChange(date), - initialVisibleMonth: () => model.initialMonth, - isOutsideRange: date => model.isOutsideRange(date), - withPortal: true, - noBorder: true, - numberOfMonths: 1, - displayFormat: model.getFormat(), - showClearDate: enableClear, - placeholder: props.placeholder, - - ...props.singleDatePickerProps - }), - rightIcon - ], - style: { - ...props.style, - ...layoutProps, - textAlign - }, - ref: ref as ForwardedRef - }); -}); +const cmp = hoistCmp.factory>( + ({model, className, ...props}, ref) => { + const layoutProps = getLayoutProps(props), + {renderValue} = model, + value = renderValue ? moment(renderValue) : null, + enableClear = withDefault(props.enableClear, false), + textAlign = withDefault(props.textAlign, 'left'), + leftIcon = withDefault(props.leftIcon, null), + rightIcon = withDefault(props.rightIcon, Icon.calendar()), + isOpen = model.popoverOpen && !props.disabled; + + return div({ + className, + items: [ + leftIcon, + singleDatePicker({ + date: value, + focused: isOpen, + onFocusChange: ({focused}) => model.setPopoverOpen(focused), + onDateChange: date => model.onDateChange(date), + initialVisibleMonth: () => model.initialMonth, + isOutsideRange: date => model.isOutsideRange(date), + withPortal: true, + noBorder: true, + numberOfMonths: 1, + displayFormat: model.getFormat(), + showClearDate: enableClear, + placeholder: props.placeholder, + + ...props.singleDatePickerProps + }), + rightIcon + ], + style: { + ...props.style, + ...layoutProps, + textAlign + }, + ref + }); + } +); diff --git a/mobile/cmp/input/Select.ts b/mobile/cmp/input/Select.ts index 2a861cbbcf..9fb9e525ed 100644 --- a/mobile/cmp/input/Select.ts +++ b/mobile/cmp/input/Select.ts @@ -6,7 +6,7 @@ */ import {HoistInputModel, HoistInputProps, useHoistInputModel} from '@xh/hoist/cmp/input'; import {box, div, hbox, span} from '@xh/hoist/cmp/layout'; -import {hoistCmp, LayoutProps, PlainObject, SelectOption} from '@xh/hoist/core'; +import {DefaultHoistProps, hoistCmp, LayoutProps, PlainObject, SelectOption} from '@xh/hoist/core'; import {Icon} from '@xh/hoist/icon'; import { reactAsyncCreatableSelect, @@ -23,7 +23,7 @@ import {throwIf, withDefault} from '@xh/hoist/utils/js'; import {createObservableRef, getLayoutProps} from '@xh/hoist/utils/react'; import debouncePromise from 'debounce-promise'; import {escapeRegExp, isEqual, isNil, isPlainObject, keyBy, merge} from 'lodash'; -import {Children, ForwardedRef, ReactNode, ReactPortal} from 'react'; +import {Children, ReactNode, ReactPortal} from 'react'; import ReactDom from 'react-dom'; import './Select.scss'; @@ -587,97 +587,99 @@ class SelectInputModel extends HoistInputModel { } } -const cmp = hoistCmp.factory(({model, className, ...props}, ref) => { - const {width, ...layoutProps} = getLayoutProps(props), - rsProps: PlainObject = { - value: model.renderValue, - - formatOptionLabel: model.formatOptionLabel, - isDisabled: props.disabled, - closeMenuOnSelect: props.closeMenuOnSelect, - hideSelectedOptions: model.hideSelectedOptions, - menuPlacement: withDefault(props.menuPlacement, 'auto'), - maxMenuHeight: props.maxMenuHeight, - noOptionsMessage: model.noOptionsMessageFn, - openMenuOnFocus: props.openMenuOnFocus || model.fullscreen, - placeholder: withDefault(props.placeholder, 'Select...'), - tabIndex: props.tabIndex, - menuShouldBlockScroll: true, - - // Minimize (or hide) bulky dropdown - components: { - DropdownIndicator: model.getDropdownIndicatorCmp(), - IndicatorSeparator: () => null - }, - - // A shared div is created lazily here as needed, appended to the body, and assigned - // a high z-index to ensure options menus render over dialogs or other modals. - menuPortalTarget: model.getOrCreatePortalDiv(), - - inputId: props.id, - classNamePrefix: 'xh-select', - theme: model.getThemeConfig(), - - onBlur: model.onBlur, - onChange: model.onSelectChange, - onFocus: model.onFocus, - filterOption: model.filterOption, - - ref: model.reactSelectRef - }; - - if (model.manageInputValue) { - rsProps.inputValue = model.inputValue || ''; - rsProps.onInputChange = model.onInputChange; - rsProps.controlShouldRenderValue = !model.hasFocus; - } +const cmp = hoistCmp.factory>( + ({model, className, ...props}, ref) => { + const {width, ...layoutProps} = getLayoutProps(props), + rsProps: PlainObject = { + value: model.renderValue, + + formatOptionLabel: model.formatOptionLabel, + isDisabled: props.disabled, + closeMenuOnSelect: props.closeMenuOnSelect, + hideSelectedOptions: model.hideSelectedOptions, + menuPlacement: withDefault(props.menuPlacement, 'auto'), + maxMenuHeight: props.maxMenuHeight, + noOptionsMessage: model.noOptionsMessageFn, + openMenuOnFocus: props.openMenuOnFocus || model.fullscreen, + placeholder: withDefault(props.placeholder, 'Select...'), + tabIndex: props.tabIndex, + menuShouldBlockScroll: true, + + // Minimize (or hide) bulky dropdown + components: { + DropdownIndicator: model.getDropdownIndicatorCmp(), + IndicatorSeparator: () => null + }, + + // A shared div is created lazily here as needed, appended to the body, and assigned + // a high z-index to ensure options menus render over dialogs or other modals. + menuPortalTarget: model.getOrCreatePortalDiv(), + + inputId: props.id, + classNamePrefix: 'xh-select', + theme: model.getThemeConfig(), + + onBlur: model.onBlur, + onChange: model.onSelectChange, + onFocus: model.onFocus, + filterOption: model.filterOption, + + ref: model.reactSelectRef + }; - if (model.asyncMode) { - rsProps.loadOptions = model.doQueryAsync; - rsProps.loadingMessage = model.loadingMessageFn; - if (model.renderValue) rsProps.defaultOptions = [model.renderValue]; - } else { - rsProps.options = model.internalOptions; - rsProps.isSearchable = model.filterMode; - } + if (model.manageInputValue) { + rsProps.inputValue = model.inputValue || ''; + rsProps.onInputChange = model.onInputChange; + rsProps.controlShouldRenderValue = !model.hasFocus; + } - if (model.creatableMode) { - rsProps.formatCreateLabel = model.createMessageFn; - } + if (model.asyncMode) { + rsProps.loadOptions = model.doQueryAsync; + rsProps.loadingMessage = model.loadingMessageFn; + if (model.renderValue) rsProps.defaultOptions = [model.renderValue]; + } else { + rsProps.options = model.internalOptions; + rsProps.isSearchable = model.filterMode; + } - if (props.menuWidth) { - rsProps.styles = { - menu: provided => ({...provided, width: `${props.menuWidth}px`}), - ...props.rsOptions?.styles - }; - } + if (model.creatableMode) { + rsProps.formatCreateLabel = model.createMessageFn; + } - const factory = model.getSelectFactory(); - merge(rsProps, props.rsOptions); + if (props.menuWidth) { + rsProps.styles = { + menu: provided => ({...provided, width: `${props.menuWidth}px`}), + ...props.rsOptions?.styles + }; + } - if (model.fullscreen) { - return ReactDom.createPortal( - fullscreenWrapper({ - model, - title: props.title, - item: box({ - item: factory(rsProps), - className, - ref: ref as ForwardedRef - }) - }), - model.getOrCreateFullscreenPortalDiv() - ) as ReactPortal; - } else { - return box({ - item: factory(rsProps), - className, - ...layoutProps, - width: withDefault(width, null), - ref: ref as ForwardedRef - }); + const factory = model.getSelectFactory(); + merge(rsProps, props.rsOptions); + + if (model.fullscreen) { + return ReactDom.createPortal( + fullscreenWrapper({ + model, + title: props.title, + item: box({ + item: factory(rsProps), + className, + ref + }) + }), + model.getOrCreateFullscreenPortalDiv() + ) as ReactPortal; + } else { + return box({ + item: factory(rsProps), + className, + ...layoutProps, + width: withDefault(width, null), + ref + }); + } } -}); +); const fullscreenWrapper = hoistCmp.factory(({model, title, children}) => { return div({ diff --git a/mobile/cmp/input/TextArea.ts b/mobile/cmp/input/TextArea.ts index 16ddeb0fef..2ec46e52bd 100644 --- a/mobile/cmp/input/TextArea.ts +++ b/mobile/cmp/input/TextArea.ts @@ -6,12 +6,11 @@ */ import {HoistInputModel, HoistInputProps, useHoistInputModel} from '@xh/hoist/cmp/input'; import {div, textarea as textareaTag} from '@xh/hoist/cmp/layout'; -import {hoistCmp, LayoutProps, StyleProps} from '@xh/hoist/core'; +import {DefaultHoistProps, hoistCmp, LayoutProps, StyleProps} from '@xh/hoist/core'; import '@xh/hoist/mobile/register'; import {withDefault} from '@xh/hoist/utils/js'; import {getLayoutProps} from '@xh/hoist/utils/react'; import './TextArea.scss'; -import {ForwardedRef} from 'react'; export interface TextAreaProps extends HoistInputProps, StyleProps, LayoutProps { value?: string; @@ -74,31 +73,33 @@ class TextAreaInputModel extends HoistInputModel { }; } -const cmp = hoistCmp.factory(({model, className, ...props}, ref) => { - const {width, height, ...layoutProps} = getLayoutProps(props); - - return div({ - item: textareaTag({ - value: model.renderValue || '', - - disabled: props.disabled, - placeholder: props.placeholder, - spellCheck: withDefault(props.spellCheck, false), - tabIndex: props.tabIndex, - - onChange: model.onChange, - onKeyDown: model.onKeyDown, - onBlur: model.onBlur, - onFocus: model.onFocus - }), - style: { - ...props.style, - ...layoutProps, - width: withDefault(width, null), - height: withDefault(height, 100) - }, - - className, - ref: ref as ForwardedRef - }); -}); +const cmp = hoistCmp.factory>( + ({model, className, ...props}, ref) => { + const {width, height, ...layoutProps} = getLayoutProps(props); + + return div({ + item: textareaTag({ + value: model.renderValue || '', + + disabled: props.disabled, + placeholder: props.placeholder, + spellCheck: withDefault(props.spellCheck, false), + tabIndex: props.tabIndex, + + onChange: model.onChange, + onKeyDown: model.onKeyDown, + onBlur: model.onBlur, + onFocus: model.onFocus + }), + style: { + ...props.style, + ...layoutProps, + width: withDefault(width, null), + height: withDefault(height, 100) + }, + + className, + ref + }); + } +); diff --git a/mobile/cmp/input/TextInput.ts b/mobile/cmp/input/TextInput.ts index 9e61a8650f..3e0a8fbac9 100644 --- a/mobile/cmp/input/TextInput.ts +++ b/mobile/cmp/input/TextInput.ts @@ -6,7 +6,7 @@ */ import {HoistInputModel, HoistInputProps, useHoistInputModel} from '@xh/hoist/cmp/input'; import {hbox} from '@xh/hoist/cmp/layout'; -import {hoistCmp, HSide, LayoutProps, StyleProps} from '@xh/hoist/core'; +import {DefaultHoistProps, hoistCmp, HSide, LayoutProps, StyleProps} from '@xh/hoist/core'; import {Icon} from '@xh/hoist/icon'; import {input} from '@xh/hoist/kit/onsen'; import {button} from '@xh/hoist/mobile/cmp/button'; @@ -15,7 +15,6 @@ import {withDefault} from '@xh/hoist/utils/js'; import {getLayoutProps} from '@xh/hoist/utils/react'; import {isEmpty} from 'lodash'; import './TextInput.scss'; -import {ForwardedRef} from 'react'; export interface TextInputProps extends HoistInputProps, StyleProps, LayoutProps { value?: string; @@ -111,44 +110,46 @@ class TextInputModel extends HoistInputModel { }; } -const cmp = hoistCmp.factory(({model, className, ...props}, ref) => { - const {width, ...layoutProps} = getLayoutProps(props); - - return hbox({ - ref: ref as ForwardedRef, - className, - style: { - ...props.style, - ...layoutProps, - width: withDefault(width, null) - }, - items: [ - input({ - value: model.renderValue || '', - - autoCapitalize: props.autoCapitalize, - autoComplete: withDefault( - props.autoComplete, - props.type === 'password' ? 'new-password' : 'off' - ), - disabled: props.disabled, - modifier: props.modifier, - placeholder: props.placeholder, - spellCheck: withDefault(props.spellCheck, false), - tabIndex: props.tabIndex, - type: props.type, - className: 'xh-text-input__input', - style: {textAlign: withDefault(props.textAlign, 'left')}, - - onInput: model.onChange, - onKeyDown: model.onKeyDown, - onBlur: model.onBlur, - onFocus: model.onFocus - }), - clearButton() - ] - }); -}); +const cmp = hoistCmp.factory>( + ({model, className, ...props}, ref) => { + const {width, ...layoutProps} = getLayoutProps(props); + + return hbox({ + ref, + className, + style: { + ...props.style, + ...layoutProps, + width: withDefault(width, null) + }, + items: [ + input({ + value: model.renderValue || '', + + autoCapitalize: props.autoCapitalize, + autoComplete: withDefault( + props.autoComplete, + props.type === 'password' ? 'new-password' : 'off' + ), + disabled: props.disabled, + modifier: props.modifier, + placeholder: props.placeholder, + spellCheck: withDefault(props.spellCheck, false), + tabIndex: props.tabIndex, + type: props.type, + className: 'xh-text-input__input', + style: {textAlign: withDefault(props.textAlign, 'left')}, + + onInput: model.onChange, + onKeyDown: model.onKeyDown, + onBlur: model.onBlur, + onFocus: model.onFocus + }), + clearButton() + ] + }); + } +); const clearButton = hoistCmp.factory(({model}) => button({ From 4a520258be411a409e2ae719829c26a9e4bc6f22 Mon Sep 17 00:00:00 2001 From: Greg Solomon Date: Fri, 28 Jun 2024 12:13:28 -0400 Subject: [PATCH 20/23] Revert added maxFiles prop to FileChooser --- desktop/cmp/filechooser/FileChooser.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/desktop/cmp/filechooser/FileChooser.ts b/desktop/cmp/filechooser/FileChooser.ts index 612b698f2b..2f0de358e9 100644 --- a/desktop/cmp/filechooser/FileChooser.ts +++ b/desktop/cmp/filechooser/FileChooser.ts @@ -33,9 +33,6 @@ export interface FileChooserProps extends HoistProps accept, maxSize, minSize, - maxFiles, targetText = 'Drag and drop files here, or click to browse...', enableMulti = true, enableAddMulti = enableMulti, @@ -91,7 +87,6 @@ export const [FileChooser, fileChooser] = hoistCmp.withFactory accept, maxSize, minSize, - maxFiles, multiple: enableAddMulti, // Passing children directly since it is not possible to pass a function via // elementFactory items prop. From d99c671c17cf5b8bf3a63752ae303f6e0d29dcc8 Mon Sep 17 00:00:00 2001 From: Greg Solomon Date: Fri, 28 Jun 2024 16:40:09 -0400 Subject: [PATCH 21/23] Changes from CR --- core/elem.ts | 43 ++++++++++++++++++----------- desktop/appcontainer/ToastSource.ts | 2 +- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/core/elem.ts b/core/elem.ts index 04c586ba49..541414f94c 100644 --- a/core/elem.ts +++ b/core/elem.ts @@ -15,7 +15,7 @@ import { ReactElement, ReactNode } from 'react'; -import {Some, Thunkable} from './types/Types'; +import {PlainObject, Some, Thunkable} from './types/Types'; /** * Alternative format for specifying React Elements in render functions. This type is designed to @@ -74,8 +74,17 @@ export type ElementSpec

= P & { $omit?: any; }; -export type ElementFactory

= ((...args: ReactNode[]) => ReactElement) & - ((arg: ElementSpec

) => ReactElement); +/** + * Union type of all known React Component types supported by Hoist. + */ +export type ReactComponent = ComponentType | keyof JSX.IntrinsicElements; + +/** + * Factory function that can create a ReactElement from an ElementSpec or children. + * Hoist alternative to JSX. + */ +export type ElementFactory

= ((arg: ElementSpec

) => ReactElement) & + ((...args: ReactNode[]) => ReactElement); /** * Create a React Element from a Component type and an ElementSpec. @@ -83,10 +92,13 @@ export type ElementFactory

= ((...args: ReactNode[]) => ReactElement(type: any, spec: ElementSpec

): ReactElement { +export function createElement( + component: C, + spec: ElementSpec> +): ReactElement, C> { const {omit, item, items, ...props} = spec; // 1) Convenience omission syntax. @@ -104,18 +116,17 @@ export function createElement

(type: any, spec: ElementSpec

): ReactEl } }); - return reactCreateElement(type, props as P, ...children) as any; + return reactCreateElement(component, props, ...children) as ReactElement, C>; } /** - * Create a factory function that can create a ReactElement from an ElementSpec. + * Create a factory function that can create a ReactElement from an ElementSpec or list of children. */ -export function elementFactory< - T extends ComponentType | keyof JSX.IntrinsicElements, - P = PropType ->(type: T): ElementFactory

{ +export function elementFactory(component: C): ElementFactory>; +export function elementFactory

(component: ReactComponent): ElementFactory

; +export function elementFactory(component: ReactComponent): ElementFactory { const ret = function (...args) { - return createElement

(type, normalizeArgs(args, type)); + return createElement(component, normalizeArgs(args, component)); }; ret.isElementFactory = true; return ret; @@ -136,9 +147,9 @@ function normalizeArgs(args: any[], type: any) { return {items: args}; } -type PropType = - T extends ComponentType +type PropType = + C extends ComponentType ? P - : T extends keyof JSX.IntrinsicElements - ? JSX.IntrinsicElements[T] + : C extends keyof JSX.IntrinsicElements + ? JSX.IntrinsicElements[C] : any; diff --git a/desktop/appcontainer/ToastSource.ts b/desktop/appcontainer/ToastSource.ts index 25693029dd..b98ffe93db 100644 --- a/desktop/appcontainer/ToastSource.ts +++ b/desktop/appcontainer/ToastSource.ts @@ -141,6 +141,6 @@ class ToastSourceLocalModel extends HoistModel { } // `OverlayToasterProps` does not include `ref` prop, so we need to add it manually -const overlayToaster = elementFactory>( +const overlayToaster = elementFactory>( OverlayToaster ); From bf4acb2c11c4f5db36310ea766d8cdb602342c25 Mon Sep 17 00:00:00 2001 From: Greg Solomon Date: Wed, 7 Aug 2024 11:33:58 -0400 Subject: [PATCH 22/23] Remove obsolete ts-expect-error --- desktop/cmp/toolbar/Toolbar.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/desktop/cmp/toolbar/Toolbar.ts b/desktop/cmp/toolbar/Toolbar.ts index 05ec37ab3f..340f4bbd29 100644 --- a/desktop/cmp/toolbar/Toolbar.ts +++ b/desktop/cmp/toolbar/Toolbar.ts @@ -106,7 +106,6 @@ const overflowBox = hoistCmp.factory({ observer: false, memo: false, render({children, minVisibleItems, collapseFrom}) { - // @ts-expect-error - TS doesn't like how we remap items to $items return overflowList({ $items: children as readonly ReactNode[], minVisibleItems, From e06da9632504984b11a903708fd1a9bbe85f669f Mon Sep 17 00:00:00 2001 From: Greg Solomon Date: Wed, 7 Aug 2024 11:47:24 -0400 Subject: [PATCH 23/23] Fix CHANGELOG --- CHANGELOG.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf828c8dc5..e494a08983 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,8 +34,7 @@ * Remove context menus from column choosers. * Typescript: Overall type improvements and cleanup. Note: `AppConfigs` with `model: false` will need to specify a `null` model type in the generic argument to `hoistCmp`, `hoistCmp.factory` or - `hoistCmp.withFactory` to avoid a type error. Additionally, prop types for components passed to - `elementFactory` are now inferred from the component itself where possible. + `hoistCmp.withFactory` to avoid a type error. ### 📚 Libraries