diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000000..be8c07099b83 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,22 @@ +{ + // Use IntelliSense to learn about possible Node.js debug attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Jest Tests", + "program": "${workspaceRoot}/node_modules/jest/bin/jest.js", + "args": [ + "--runInBand" + ], + "runtimeArgs": [ + "--harmony" + ], + "sourceMaps": true, + "cwd": "${workspaceRoot}" + }, + ] +} diff --git a/js/renovation/button.tsx b/js/renovation/button.tsx index 2121e782ba88..a2b9963a1cc3 100644 --- a/js/renovation/button.tsx +++ b/js/renovation/button.tsx @@ -133,29 +133,16 @@ export class ButtonInput extends WidgetInput { // tslint:disable-next-line: max-classes-per-file @Component({ - name: 'Button', components: [], - viewModel: viewModelFunction, + name: 'Button', view: viewFunction, + viewModel: viewModelFunction, }) export default class Button extends JSXComponent { @Ref() contentRef!: HTMLDivElement; @Ref() submitInputRef!: HTMLInputElement; - @Effect() - submitEffect() { - const namespace = 'UIFeedback'; - const { onSubmit } = this.props; - - click.on(this.submitInputRef, (e) => { - onSubmit?.(e); - e.stopPropagation(); - }, { namespace }); - - return () => click.off(this.submitInputRef, { namespace }); - } - onActive(event: Event) { const { useInkRipple } = this.props; const config = getInkRippleConfig(this.props); @@ -184,4 +171,17 @@ export default class Button extends JSXComponent { this.onWidgetClick(e); } } + + @Effect() + submitEffect() { + const namespace = 'UIFeedback'; + const { onSubmit } = this.props; + + click.on(this.submitInputRef, (e) => { + onSubmit?.(e); + e.stopPropagation(); + }, { namespace }); + + return () => click.off(this.submitInputRef, { namespace }); + } } diff --git a/js/renovation/widget.tsx b/js/renovation/widget.tsx index 3f9a0b89b794..fb6a94cd4794 100644 --- a/js/renovation/widget.tsx +++ b/js/renovation/widget.tsx @@ -16,15 +16,14 @@ import { each } from '../core/utils/iterator'; import { extend } from '../core/utils/extend'; import { focusable } from '../ui/widget/selectors'; import { isFakeClickEvent } from '../events/utils'; -import { hasWindow } from '../core/utils/window'; const getStyles = ({ width, height }) => { const computedWidth = typeof width === 'function' ? width() : width; const computedHeight = typeof height === 'function' ? height() : height; return { - width: computedWidth ?? void 0, height: computedHeight ?? void 0, + width: computedWidth ?? void 0, }; }; @@ -68,6 +67,7 @@ const getCssClasses = (model: Partial & Partial) => { model._active && className.push('dx-state-active'); model._hovered && isHoverable && !model._active && className.push('dx-state-hover'); model.rtlEnabled && className.push('dx-rtl'); + model.onVisibilityChange && className.push('dx-visibility-change-handler'); model.elementAttr?.class && className.push(model.elementAttr.class); return className.join(' '); @@ -93,19 +93,21 @@ export const viewModelFunction = ({ tabIndex, visible, width, + onVisibilityChange, }, widgetRef, }: Widget) => { const styles = getStyles({ width, height }); const attrsWithoutClass = getAttributes({ - elementAttr, accessKey: focusStateEnabled && !disabled && accessKey, + elementAttr, }); const arias = getAria({ ...aria, disabled, hidden: !visible }); const cssClasses = getCssClasses({ - disabled, visible, _focused, _active, _hovered, rtlEnabled, - elementAttr, hoverStateEnabled, focusStateEnabled, className, + _active, _focused, _hovered, className, + disabled, elementAttr, focusStateEnabled, hoverStateEnabled, + onVisibilityChange, rtlEnabled, visible, }); return { @@ -143,11 +145,11 @@ export const viewFunction = (viewModel: any) => { export class WidgetInput { @OneWay() _feedbackHideTimeout?: number = 400; @OneWay() _feedbackShowTimeout?: number = 30; - @OneWay() _visibilityChanged?: (args: any) => undefined; @OneWay() accessKey?: string | null = null; @OneWay() activeStateEnabled?: boolean = false; @OneWay() activeStateUnit?: string; @OneWay() aria?: any = {}; + @Slot() children?: any; @OneWay() className?: string | undefined = ''; @OneWay() clickArgs?: any = {}; @OneWay() disabled?: boolean = false; @@ -158,26 +160,24 @@ export class WidgetInput { @OneWay() hoverStateEnabled?: boolean = false; @OneWay() name?: string = ''; @OneWay() onActive?: (e: any) => any = (() => undefined); + @Event() onClick?: (e: any) => void = (() => { }); @OneWay() onDimensionChanged?: () => any = (() => undefined); @OneWay() onInactive?: (e: any) => any = (() => undefined); - @OneWay() onKeyPress?: (e: any, options: any) => any = (() => undefined); @OneWay() onKeyboardHandled?: (args: any) => any | undefined; + @OneWay() onKeyPress?: (e: any, options: any) => any = (() => undefined); + @OneWay() onVisibilityChange?: (args: boolean) => undefined; @OneWay() rtlEnabled?: boolean = config().rtlEnabled; @OneWay() tabIndex?: number = 0; @OneWay() visible?: boolean = true; @OneWay() width?: string | number | null = null; - - @Slot() children?: any; - - @Event() onClick?: (e: any) => void = (() => { }); } // tslint:disable-next-line: max-classes-per-file @Component({ - name: 'Widget', components: [], - viewModel: viewModelFunction, + name: 'Widget', view: viewFunction, + viewModel: viewModelFunction, }) export default class Widget extends JSXComponent { @@ -188,38 +188,6 @@ export default class Widget extends JSXComponent { @Ref() widgetRef!: HTMLDivElement; - @Effect() - visibilityEffect() { - const { name, _visibilityChanged, visible } = this.props; - const namespace = `${name}VisibilityChange`; - - if (_visibilityChanged !== undefined && hasWindow()) { - visibility.on(this.widgetRef, - () => visible && _visibilityChanged!(true), - () => visible && _visibilityChanged!(false), - { namespace }, - ); - - return () => visibility.off(this.widgetRef, { namespace }); - } - - return null; - } - - @Effect() - resizeEffect() { - const namespace = `${this.props.name}VisibilityChange`; - const { onDimensionChanged } = this.props; - - if (onDimensionChanged) { - resize.on(this.widgetRef, onDimensionChanged, { namespace }); - - return () => resize.off(this.widgetRef, { namespace }); - } - - return null; - } - @Effect() accessKeyEffect() { const namespace = 'UIFeedback'; @@ -241,26 +209,6 @@ export default class Widget extends JSXComponent { return null; } - @Effect() - hoverEffect() { - const namespace = 'UIFeedback'; - const { activeStateUnit, hoverStateEnabled, disabled } = this.props; - const selector = activeStateUnit; - const isHoverable = hoverStateEnabled && !disabled; - - if (isHoverable) { - hover.on(this.widgetRef, - () => !this._active && (this._hovered = true), - () => this._hovered = false, - { selector, namespace }, - ); - - return () => hover.off(this.widgetRef, { selector, namespace }); - } - - return null; - } - @Effect() activeEffect() { const { @@ -280,10 +228,10 @@ export default class Widget extends JSXComponent { this._active = false; onInactive?.(event); }, { - showTimeout: _feedbackShowTimeout, hideTimeout: _feedbackHideTimeout, - selector, namespace, + selector, + showTimeout: _feedbackShowTimeout, }, ); @@ -293,6 +241,16 @@ export default class Widget extends JSXComponent { return null; } + @Effect() + clickEffect() { + const { name, clickArgs } = this.props; + const namespace = name; + + dxClick.on(this.widgetRef, () => this.props.onClick!(clickArgs), { namespace }); + + return () => dxClick.off(this.widgetRef, { namespace }); + } + @Effect() focusEffect() { const { disabled, focusStateEnabled, name } = this.props; @@ -304,8 +262,8 @@ export default class Widget extends JSXComponent { e => !e.isDefaultPrevented() && (this._focused = true), e => !e.isDefaultPrevented() && (this._focused = false), { - namespace, isFocusable: el => focusable(null, el), + namespace, }, ); @@ -316,13 +274,23 @@ export default class Widget extends JSXComponent { } @Effect() - clickEffect() { - const { name, clickArgs } = this.props; - const namespace = name; + hoverEffect() { + const namespace = 'UIFeedback'; + const { activeStateUnit, hoverStateEnabled, disabled } = this.props; + const selector = activeStateUnit; + const isHoverable = hoverStateEnabled && !disabled; - dxClick.on(this.widgetRef, () => this.props.onClick!(clickArgs), { namespace }); + if (isHoverable) { + hover.on(this.widgetRef, + () => !this._active && (this._hovered = true), + () => this._hovered = false, + { selector, namespace }, + ); - return () => dxClick.off(this.widgetRef, { namespace }); + return () => hover.off(this.widgetRef, { selector, namespace }); + } + + return null; } @Effect() @@ -338,4 +306,36 @@ export default class Widget extends JSXComponent { return null; } + + @Effect() + resizeEffect() { + const namespace = `${this.props.name}VisibilityChange`; + const { onDimensionChanged } = this.props; + + if (onDimensionChanged) { + resize.on(this.widgetRef, onDimensionChanged, { namespace }); + + return () => resize.off(this.widgetRef, { namespace }); + } + + return null; + } + + @Effect() + visibilityEffect() { + const { name, onVisibilityChange } = this.props; + + const namespace = `${name}VisibilityChange`; + if (onVisibilityChange) { + visibility.on(this.widgetRef, + () => onVisibilityChange!(true), + () => onVisibilityChange!(false), + { namespace }, + ); + + return () => visibility.off(this.widgetRef, { namespace }); + } + + return null; + } } diff --git a/testing/jest/button.tests.tsx b/testing/jest/button.tests.tsx index 40a75d059302..620a24ab7875 100644 --- a/testing/jest/button.tests.tsx +++ b/testing/jest/button.tests.tsx @@ -280,6 +280,20 @@ describe('Button', () => { }); }); + describe('visible', () => { + it('should pass the default value into Widget component', () => { + const tree = render(); + + expect(tree.find(Widget).prop('visible')).toBe(true); + }); + + it('should pass the custom value into Widget component', () => { + const tree = render({ visible: false }); + + expect(tree.find(Widget).prop('visible')).toBe(false); + }); + }); + describe('focusStateEnabled', () => { it('should pass a default value into Widget component', () => { const button = render(); diff --git a/testing/jest/utils/events-mock.js b/testing/jest/utils/events-mock.js index b2810aab92a3..5b28e04873bf 100644 --- a/testing/jest/utils/events-mock.js +++ b/testing/jest/utils/events-mock.js @@ -20,9 +20,11 @@ export const EVENT = { click: 'click', dxClick: 'dxclick', focus: 'focusin', + hiding: 'dxhiding', hoverEnd: 'dxhoverend', hoverStart: 'dxhoverstart', - inactive: 'dxinactive' + inactive: 'dxinactive', + shown: 'dxshown', }; export const defaultEvent = { @@ -77,5 +79,8 @@ eventsEngine.on = (...args) => { }); }; -eventsEngine.off = (el, event) => eventHandlers[event] = []; +eventsEngine.off = (...args) => { + const event = args[1].split('.')[0]; + eventHandlers[event] = []; +}; keyboard.off = id => delete keyboardHandlers[id]; diff --git a/testing/jest/widget.tests.tsx b/testing/jest/widget.tests.tsx index f1e651cae68b..7c4427c9a7e1 100644 --- a/testing/jest/widget.tests.tsx +++ b/testing/jest/widget.tests.tsx @@ -362,6 +362,29 @@ describe('Widget', () => { }); }); + describe('Callbacks', () => { + describe('onVisibilityChanged', () => { + const onVisibilityChange = jest.fn(); + it('should add special css class when `onVisibilityChange` handler is defined', () => { + const widget = render({ onVisibilityChange }); + + expect(widget.exists('.dx-visibility-change-handler')).toBe(true); + }); + + it('should be called on `dxhiding` and `dxshown` events and special css class is attached', () => { + render({ onVisibilityChange }); + + emit(EVENT.hiding); + expect(onVisibilityChange).toHaveBeenCalledTimes(1); + expect(onVisibilityChange).toHaveBeenLastCalledWith(false); + + emit(EVENT.shown); + expect(onVisibilityChange).toHaveBeenCalledTimes(2); + expect(onVisibilityChange).toHaveBeenLastCalledWith(true); + }); + }); + }); + it('should have dx-widget class', () => { const tree = render(); diff --git a/tslint.json b/tslint.json index a55b863de166..5783ee56f6bf 100644 --- a/tslint.json +++ b/tslint.json @@ -19,7 +19,7 @@ "import-spacing": true, "semicolon": true, "max-line-length": [true, 100], - "object-literal-sort-keys": false, + "object-literal-sort-keys": true, "ordered-imports": false, "jsx-no-lambda": false, "jsx-boolean-value": false, @@ -31,7 +31,21 @@ "object-shorthand-properties-first": false, "prefer-array-literal": [true, { "allow-type-parameters": true }], "array-type": false, - "member-ordering": false, + "member-ordering": [ + true, { + "order": [ + "public-static-field", + "public-instance-field", + "public-constructor", + "private-static-field", + "private-instance-field", + "private-constructor", + "public-instance-method", + "protected-instance-method", + "private-instance-method" + ], + "alphabetize": true + }], "jsx-wrap-multiline": false, "no-this-assignment": [true, {"allow-destructuring": true}], "ter-indent": [true, 4, { "SwitchCase": 1 }],