Skip to content

Commit

Permalink
Panel, Modal, Overlay: Optional prop that disables bodyscroll block o…
Browse files Browse the repository at this point in the history
…n touch devices (#11339)

* modal, panel, overlay: ios allow bodyscroll

* Change files

* formatting

* renamed ios -> touch, new event handler instead of boolean parameter

* Update change files

* Update api description
  • Loading branch information
Dmitriy authored and JasonGore committed Dec 19, 2019
1 parent b5dd371 commit 10045d1
Show file tree
Hide file tree
Showing 11 changed files with 103 additions and 7 deletions.
8 changes: 8 additions & 0 deletions change/@uifabric-utilities-2019-11-28-22-34-32-master.json
@@ -0,0 +1,8 @@
{
"type": "minor",
"comment": "scroll.ts: added allowOverscrollOnElement, alternative to allowScrollOnElement that allows body scroll on touch devices",
"packageName": "@uifabric/utilities",
"email": "dmitriy.ravdin@siemens.com",
"commit": "00c16da9f0d9ee3de29c6de6a0aa32d50397cdcc",
"date": "2019-11-28T21:34:32.268Z"
}
8 changes: 8 additions & 0 deletions change/office-ui-fabric-react-2019-11-28-22-34-32-master.json
@@ -0,0 +1,8 @@
{
"type": "minor",
"comment": "Modal, Panel, Overlay: added optional prop allowTouchBodyScroll that allows body scroll on touch devices",
"packageName": "office-ui-fabric-react",
"email": "dmitriy.ravdin@siemens.com",
"commit": "00c16da9f0d9ee3de29c6de6a0aa32d50397cdcc",
"date": "2019-11-28T21:34:30.900Z"
}
Expand Up @@ -5538,6 +5538,7 @@ export interface IModal {

// @public (undocumented)
export interface IModalProps extends React.ClassAttributes<ModalBase>, IWithResponsiveModeState, IAccessiblePopupProps {
allowTouchBodyScroll?: boolean;
className?: string;
componentRef?: IRefObject<IModal>;
containerClassName?: string;
Expand Down Expand Up @@ -5746,6 +5747,7 @@ export interface IOverlay {

// @public (undocumented)
export interface IOverlayProps extends React.HTMLAttributes<HTMLElement> {
allowTouchBodyScroll?: boolean;
className?: string;
componentRef?: IRefObject<IOverlay>;
isDarkThemed?: boolean;
Expand Down Expand Up @@ -5825,6 +5827,7 @@ export interface IPanelHeaderRenderer extends IRenderFunction<IPanelProps> {
//
// @public (undocumented)
export interface IPanelProps extends React.HTMLAttributes<PanelBase> {
allowTouchBodyScroll?: boolean;
className?: string;
closeButtonAriaLabel?: string;
// @deprecated
Expand Down Expand Up @@ -8237,6 +8240,7 @@ export const Overlay: React.StatelessComponent<IOverlayProps>;

// @public (undocumented)
export class OverlayBase extends BaseComponent<IOverlayProps, {}> {
constructor(props: IOverlayProps);
// (undocumented)
componentDidMount(): void;
// (undocumented)
Expand Down
@@ -1,5 +1,13 @@
import * as React from 'react';
import { BaseComponent, classNamesFunction, getId, allowScrollOnElement, KeyCodes, elementContains } from '../../Utilities';
import {
BaseComponent,
classNamesFunction,
getId,
allowScrollOnElement,
allowOverscrollOnElement,
KeyCodes,
elementContains
} from '../../Utilities';
import { FocusTrapZone, IFocusTrapZone } from '../FocusTrapZone/index';
import { animationDuration } from './Modal.styles';
import { IModalProps, IModalStyleProps, IModalStyles, IModal } from './Modal.types';
Expand Down Expand Up @@ -47,6 +55,7 @@ export class ModalBase extends BaseComponent<IModalProps, IDialogState> implemen
private _scrollableContent: HTMLDivElement | null;
private _lastSetX: number;
private _lastSetY: number;
private _allowTouchBodyScroll: boolean;
private _hasRegisteredKeyUp: boolean;

constructor(props: IModalProps) {
Expand All @@ -66,6 +75,9 @@ export class ModalBase extends BaseComponent<IModalProps, IDialogState> implemen
this._warnDeprecations({
onLayerDidMount: 'layerProps.onLayerDidMount'
});

const { allowTouchBodyScroll = false } = this.props;
this._allowTouchBodyScroll = allowTouchBodyScroll;
}

// tslint:disable-next-line function-name
Expand Down Expand Up @@ -238,7 +250,14 @@ export class ModalBase extends BaseComponent<IModalProps, IDialogState> implemen
onDismiss={onDismiss}
>
<div className={classNames.root}>
{!isModeless && <Overlay isDarkThemed={isDarkOverlay} onClick={isBlocking ? undefined : (onDismiss as any)} {...overlay} />}
{!isModeless && (
<Overlay
isDarkThemed={isDarkOverlay}
onClick={isBlocking ? undefined : (onDismiss as any)}
allowTouchBodyScroll={this._allowTouchBodyScroll}
{...overlay}
/>
)}
{dragOptions ? (
<DraggableZone
handleSelector={dragOptions.dragHandleSelector || `.${classNames.main.split(' ')[0]}`}
Expand Down Expand Up @@ -270,7 +289,11 @@ export class ModalBase extends BaseComponent<IModalProps, IDialogState> implemen
// Allow the user to scroll within the modal but not on the body
private _allowScrollOnModal = (elt: HTMLDivElement | null): void => {
if (elt) {
allowScrollOnElement(elt, this._events);
if (this._allowTouchBodyScroll) {
allowOverscrollOnElement(elt, this._events);
} else {
allowScrollOnElement(elt, this._events);
}
} else {
this._events.off(this._scrollableContent);
}
Expand Down
Expand Up @@ -153,6 +153,12 @@ export interface IModalProps extends React.ClassAttributes<ModalBase>, IWithResp
* The options to make the modal draggable
*/
dragOptions?: IDragOptions;

/**
* Allow body scroll on content and overlay on touch devices. Changing after mounting has no effect.
* @defaultvalue false
*/
allowTouchBodyScroll?: boolean;
}

/**
Expand Down
Expand Up @@ -5,12 +5,21 @@ import { IOverlayProps, IOverlayStyleProps, IOverlayStyles } from './Overlay.typ
const getClassNames = classNamesFunction<IOverlayStyleProps, IOverlayStyles>();

export class OverlayBase extends BaseComponent<IOverlayProps, {}> {
private _allowTouchBodyScroll: boolean;

constructor(props: IOverlayProps) {
super(props);

const { allowTouchBodyScroll = false } = this.props;
this._allowTouchBodyScroll = allowTouchBodyScroll;
}

public componentDidMount(): void {
disableBodyScroll();
!this._allowTouchBodyScroll && disableBodyScroll();
}

public componentWillUnmount(): void {
enableBodyScroll();
!this._allowTouchBodyScroll && enableBodyScroll();
}

public render(): JSX.Element {
Expand Down
Expand Up @@ -39,6 +39,12 @@ export interface IOverlayProps extends React.HTMLAttributes<HTMLElement> {
isDarkThemed?: boolean;

onClick?: () => void;

/**
* Allow body scroll on touch devices. Changing after mounting has no effect.
* @defaultvalue false
*/
allowTouchBodyScroll?: boolean;
}

/**
Expand Down
Expand Up @@ -6,6 +6,7 @@ import { Popup } from '../../Popup';
import { getTheme, IconFontSizes, IProcessedStyleSet } from '../../Styling';
import {
allowScrollOnElement,
allowOverscrollOnElement,
BaseComponent,
classNamesFunction,
divProperties,
Expand Down Expand Up @@ -45,6 +46,7 @@ export class PanelBase extends BaseComponent<IPanelProps, IPanelState> implement
private _classNames: IProcessedStyleSet<IPanelStyles>;
private _scrollableContent: HTMLDivElement | null;
private _animationCallback: number | null = null;
private _allowTouchBodyScroll: boolean;

public static getDerivedStateFromProps(nextProps: Readonly<IPanelProps>, prevState: Readonly<IPanelState>): Partial<IPanelState> | null {
if (nextProps.isOpen === undefined) {
Expand All @@ -68,6 +70,9 @@ export class PanelBase extends BaseComponent<IPanelProps, IPanelState> implement
constructor(props: IPanelProps) {
super(props);

const { allowTouchBodyScroll = false } = this.props;
this._allowTouchBodyScroll = allowTouchBodyScroll;

this._warnDeprecations({
ignoreExternalFocusing: 'focusTrapZoneProps',
forceFocusInsideTrap: 'focusTrapZoneProps',
Expand Down Expand Up @@ -169,7 +174,7 @@ export class PanelBase extends BaseComponent<IPanelProps, IPanelState> implement
type
});

const { _classNames } = this;
const { _classNames, _allowTouchBodyScroll } = this;

let overlay;
if (isBlocking && isOpen) {
Expand All @@ -178,6 +183,7 @@ export class PanelBase extends BaseComponent<IPanelProps, IPanelState> implement
className={_classNames.overlay}
isDarkThemed={false}
onClick={isLightDismiss ? onLightDismissClick : undefined}
allowTouchBodyScroll={_allowTouchBodyScroll}
{...overlayProps}
/>
);
Expand Down Expand Up @@ -268,7 +274,11 @@ export class PanelBase extends BaseComponent<IPanelProps, IPanelState> implement
// Allow the user to scroll within the panel but not on the body
private _allowScrollOnPanel = (elt: HTMLDivElement | null): void => {
if (elt) {
allowScrollOnElement(elt, this._events);
if (this._allowTouchBodyScroll) {
allowOverscrollOnElement(elt, this._events);
} else {
allowScrollOnElement(elt, this._events);
}
} else {
this._events.off(this._scrollableContent);
}
Expand Down
Expand Up @@ -225,6 +225,12 @@ export interface IPanelProps extends React.HTMLAttributes<PanelBase> {
* @deprecated Serves no function.
*/
componentId?: string;

/**
* Allow body scroll on content and overlay on touch devices. Changing after mounting has no effect.
* @defaultvalue false
*/
allowTouchBodyScroll?: boolean;
}

/**
Expand Down
3 changes: 3 additions & 0 deletions packages/utilities/etc/utilities.api.md
Expand Up @@ -17,6 +17,9 @@ export function addDirectionalKeyCode(which: number): void;
// @public
export function addElementAtIndex<T>(array: T[], index: number, itemToAdd: T): T[];

// @public
export const allowOverscrollOnElement: (element: HTMLElement | null, events: EventGroup) => void;

// @public
export const allowScrollOnElement: (element: HTMLElement | null, events: EventGroup) => void;

Expand Down
13 changes: 13 additions & 0 deletions packages/utilities/src/scroll.ts
Expand Up @@ -84,6 +84,19 @@ const _makeElementScrollAllower = () => {
*/
export const allowScrollOnElement = _makeElementScrollAllower();

/**
* Same as allowScrollOnElement but does not prevent overscrolling.
*/
export const allowOverscrollOnElement = (element: HTMLElement | null, events: EventGroup): void => {
if (!element) {
return;
}
const _allowElementScroll = (event: TouchEvent) => {
event.stopPropagation();
};
events.on(element, 'touchmove', _allowElementScroll, { passive: false });
};

const _disableIosBodyScroll = (event: TouchEvent) => {
event.preventDefault();
};
Expand Down

0 comments on commit 10045d1

Please sign in to comment.