Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Panel, Modal, Overlay: Optional prop that disables bodyscroll block on touch devices #11339

Merged
merged 8 commits into from Dec 19, 2019
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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: extended event handler provided by _makeElementScrollAllower with allowIosOverscroll argument to make blocking of body scroll on touch devices optional",
"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 components: added optional prop allowIosBodyScroll to allow 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 @@ -5531,6 +5531,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 @@ -5739,6 +5740,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 @@ -5818,6 +5820,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 @@ -8217,6 +8220,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. 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 ios. 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. 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