wix-style-react
Version:
663 lines (584 loc) • 25.3 kB
JavaScript
import _extends from "@babel/runtime/helpers/extends";
import _objectWithoutProperties from "@babel/runtime/helpers/objectWithoutProperties";
import _classCallCheck from "@babel/runtime/helpers/classCallCheck";
import _createClass from "@babel/runtime/helpers/createClass";
import _assertThisInitialized from "@babel/runtime/helpers/assertThisInitialized";
import _inherits from "@babel/runtime/helpers/inherits";
import _possibleConstructorReturn from "@babel/runtime/helpers/possibleConstructorReturn";
import _getPrototypeOf from "@babel/runtime/helpers/getPrototypeOf";
import _defineProperty from "@babel/runtime/helpers/defineProperty";
var _excluded = ["className", "horizontalScroll", "style"];
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) { symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); } keys.push.apply(keys, symbols); } return keys; }
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }
function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }
function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } }
import React from 'react';
import { WixStyleReactContext } from '../WixStyleReactProvider/context';
import PropTypes from 'prop-types';
import { ResizeSensor } from 'css-element-queries';
import { st, classes, stVars, vars } from './Page.st.css';
import { PageContext } from './PageContext';
import PageHeader from '../PageHeader';
import PageSection from '../PageSection';
import Content from './Content';
import Tail from './Tail';
import { PageSticky } from './PageSticky';
import FixedFooter from './FixedFooter';
import ScrollableContainer from '../common/ScrollableContainer';
import { ScrollableContainerCommonProps } from '../common/PropTypes/ScrollableContainerCommon';
/*
* Page structure without mini-header-overlay:
*
* + PageWrapper (Horizontal Scroll) --
* | +- Page --------------------------
* | | +-- ScrollableContainer (Vertical Scroll)
* | | | +-- MinimizationPlaceholder
* | | | |
* | | | +-----------------------------
* | | | +-- HeaderContainer ------ (position: fixed - when minimized)
* | | | | +-- Page.Header ------------
* | | | | |
* | | | | +---------------------------
* | | | | +-- Page.Tail --------------
* | | | | |
* | | | | +---------------------------
* | | | +-----------------------------
* | | | +-- ContentWrapper------------
* | | | | +-- Page.FixedContent (Deprecated)
* | | | | |
* | | | | +---------------------------
* | | | | +-- Page.Content -----------
* | | | | | +-- Page.Section ---------
* | | | | |
* | | | | +---------------------------
* | | | +-----------------------------
* | | +-------------------------------
* | +--------------------------------- (Page - End)
* +----------------------------------- (PageWrapper - End)
*
* - ScrollableContainer has a data-classnamed 'scrollable-content', and should NOT be renamed, since
* Tooltip is hard-coded-ly using a selector like this: [data-class="page-scrollable-content"]
*/
var Page = /*#__PURE__*/function (_React$PureComponent) {
_inherits(Page, _React$PureComponent);
var _super = _createSuper(Page);
function Page(props) {
var _this;
_classCallCheck(this, Page);
_this = _super.call(this, props);
_defineProperty(_assertThisInitialized(_this), "_renderFixedFooter", function () {
var children = _this.props.children;
var childrenObject = getChildrenObject(children);
var FixedFooterChild = childrenObject.FixedFooter,
PageContent = childrenObject.PageContent;
var contentFullScreen = PageContent && PageContent.props.fullScreen;
var pageDimensionsStyle = contentFullScreen ? null : _this._getPageDimensionsStyle();
if (FixedFooterChild) {
return /*#__PURE__*/React.createElement("div", {
className: classes.fixedFooter,
ref: function ref(_ref) {
_this.footerWrapperRef = _ref;
},
style: pageDimensionsStyle
}, /*#__PURE__*/React.cloneElement(FixedFooterChild, {}));
}
});
_this.scrollableContainerRef = /*#__PURE__*/React.createRef();
_this._handleScroll = _this._handleScroll.bind(_assertThisInitialized(_this));
_this._handleWidthResize = _this._handleWidthResize.bind(_assertThisInitialized(_this));
_this._handleWindowResize = _this._handleWindowResize.bind(_assertThisInitialized(_this));
_this._calculateComponentsHeights = _this._calculateComponentsHeights.bind(_assertThisInitialized(_this));
_this.state = {
headerContainerHeight: 0,
headerWrapperHeight: 0,
tailHeight: 0,
footerHeight: 0,
minimized: false
};
return _this;
}
_createClass(Page, [{
key: "componentDidMount",
value: function componentDidMount() {
this._calculateComponentsHeights();
this.contentResizeListener = new ResizeSensor(this._getScrollContainer().childNodes[0], this._handleWidthResize);
this._handleWidthResize();
window.addEventListener('resize', this._handleWindowResize); // TODO: Hack to fix cases where initial measurement of headerWrapperHeight is not correct (need to investigate)
// Happens in PageTestStories -> PageWithScroll -> 5. Scroll - Trigger Mini Header
// Maybe there is a transition
var ARBITRARY_SHORT_DURATION_MS = 100;
setTimeout(this._calculateComponentsHeights, ARBITRARY_SHORT_DURATION_MS); // This is done for backward compatibility only,
// Notifying current users that passed the `scrollableContentRef` prop about the ref current value.
// New users should be encouraged to use the new event handlers onScrollChanged/onScrollAreaChanged
// according to their use case.
this.props.scrollableContentRef && this.props.scrollableContentRef(this.scrollableContainerRef.current);
}
}, {
key: "componentDidUpdate",
value: function componentDidUpdate(prevProps) {
this._calculateComponentsHeights();
}
}, {
key: "componentWillUnmount",
value: function componentWillUnmount() {
window.removeEventListener('resize', this._handleWindowResize);
this.contentResizeListener.detach(this._handleResize);
}
}, {
key: "_getNamedChildren",
value: function _getNamedChildren() {
return getChildrenObject(this.props.children);
}
}, {
key: "_calculateComponentsHeights",
value: function _calculateComponentsHeights() {
var _this$state = this.state,
headerContainerHeight = _this$state.headerContainerHeight,
headerWrapperHeight = _this$state.headerWrapperHeight,
tailHeight = _this$state.tailHeight,
pageHeight = _this$state.pageHeight,
footerHeight = _this$state.footerHeight,
minimized = _this$state.minimized;
var newHeaderWrapperHeight = this.headerWrapperRef && !minimized ? this.headerWrapperRef.getBoundingClientRect().height : headerWrapperHeight;
var newHeaderContainerHeight = this.headerWrapperRef && !minimized ? this.headerContainerRef.getBoundingClientRect().height : headerContainerHeight;
var newTailHeight = this.pageHeaderTailRef ? this.pageHeaderTailRef.offsetHeight : 0;
var newPageHeight = this.pageRef ? this.pageRef.offsetHeight : 0;
var newFooterHeight = this.footerWrapperRef ? this.footerWrapperRef.offsetHeight : 0;
if (headerContainerHeight !== newHeaderContainerHeight || headerWrapperHeight !== newHeaderWrapperHeight || tailHeight !== newTailHeight || pageHeight !== newPageHeight || footerHeight !== newFooterHeight) {
this.setState({
headerContainerHeight: newHeaderContainerHeight,
headerWrapperHeight: newHeaderWrapperHeight,
tailHeight: newTailHeight,
pageHeight: newPageHeight,
footerHeight: newFooterHeight
});
}
}
}, {
key: "_getScrollContainer",
value: function _getScrollContainer() {
return this.scrollableContainerRef.current;
}
}, {
key: "_getMinimizedHeaderWrapperHeight",
value: function _getMinimizedHeaderWrapperHeight() {
if (!this._hasHeader()) {
return 0;
}
return this._hasTail() ? parseInt(stVars.minimizedHeaderWrapperWithTailHeightPx, 10) : parseInt(stVars.minimizedHeaderWrapperHeightPx, 10);
}
}, {
key: "_getMinimizationDiff",
value: function _getMinimizationDiff() {
var headerWrapperHeight = this.state.headerWrapperHeight;
return headerWrapperHeight ? headerWrapperHeight - this._getMinimizedHeaderWrapperHeight() : null;
}
}, {
key: "_handleScroll",
value: function _handleScroll(e) {
var containerScrollTop = this._getScrollContainer().scrollTop;
var minimized = this.state.minimized;
var minimizationDiff = this._getMinimizationDiff();
var nextDisplayMiniHeader = minimizationDiff && containerScrollTop >= minimizationDiff;
if (minimized !== nextDisplayMiniHeader) {
this.setState({
minimized: nextDisplayMiniHeader
});
}
var onScrollChanged = this.props.scrollProps.onScrollChanged;
if (onScrollChanged) {
onScrollChanged(e);
}
}
}, {
key: "_handleWidthResize",
value: function _handleWidthResize() {}
}, {
key: "_handleWindowResize",
value: function _handleWindowResize() {
// TODO: Optimize : https://developer.mozilla.org/en-US/docs/Web/Events/resize
// Taken from here: https://github.com/kunokdev/react-window-size-listener/blob/d64c077fba4d4e0ce060464078c5fc19620528e6/src/index.js#L66
var windowHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight;
if (this.state.windowHeight !== windowHeight) {
// We are not using windowHeight directly, since we need to measure the `<Page/>`'s height,
// But we hold it in the state to avoid rendering when only window.width changes
this.setState({
windowHeight: windowHeight
});
}
}
}, {
key: "_safeGetChildren",
value: function _safeGetChildren(element) {
if (!element || !element.props || !element.props.children) {
return [];
}
return element.props.children;
}
}, {
key: "_getPageDimensionsStyle",
value: function _getPageDimensionsStyle() {
var _this$props = this.props,
maxWidth = _this$props.maxWidth,
sidePadding = _this$props.sidePadding; // TODO: Simplify - maxWidth is always truthy (from defaultProp)
if (!maxWidth && !sidePadding && sidePadding !== 0) {
return null;
}
var styles = {};
if (maxWidth) {
styles.maxWidth = "".concat(maxWidth, "px");
}
if (sidePadding || sidePadding === 0) {
styles.paddingLeft = "".concat(sidePadding, "px");
styles.paddingRight = "".concat(sidePadding, "px");
}
return styles;
}
}, {
key: "_hasBackgroundImage",
value: function _hasBackgroundImage() {
return !!this.props.backgroundImageUrl;
}
}, {
key: "_hasGradientClassName",
value: function _hasGradientClassName() {
return !!this.props.gradientClassName && !this.props.backgroundImageUrl;
}
}, {
key: "_renderContentHorizontalLayout",
value: function _renderContentHorizontalLayout(props) {
var _this$_getNamedChildr = this._getNamedChildren(),
PageContent = _this$_getNamedChildr.PageContent;
var contentFullScreen = PageContent && PageContent.props.fullScreen;
var className = props.className,
horizontalScroll = props.horizontalScroll,
style = props.style,
rest = _objectWithoutProperties(props, _excluded);
var pageDimensionsStyle = contentFullScreen ? null : this._getPageDimensionsStyle();
return /*#__PURE__*/React.createElement("div", _extends({
className: st(classes.contentHorizontalLayout, {
contentFullWidth: contentFullScreen,
horizontalScroll: horizontalScroll
}, className),
style: _objectSpread(_objectSpread({}, pageDimensionsStyle), style)
}, rest), props.children);
}
}, {
key: "_renderHeader",
value: function _renderHeader() {
var _this2 = this;
var minimized = this.state.minimized;
var _this$_getNamedChildr2 = this._getNamedChildren(),
PageHeaderChild = _this$_getNamedChildr2.PageHeader;
var dataHook = 'page-header-wrapper';
return PageHeaderChild && /*#__PURE__*/React.createElement(WixStyleReactContext.Consumer, {
key: dataHook
}, function (_ref2) {
var reducedSpacingAndImprovedLayout = _ref2.reducedSpacingAndImprovedLayout;
return /*#__PURE__*/React.createElement("div", {
"data-hook": dataHook,
className: st(classes.headerWrapper, {
reducedSpacingAndImprovedLayout: reducedSpacingAndImprovedLayout,
minimized: minimized
}),
ref: function ref(_ref3) {
return _this2.headerWrapperRef = _ref3;
}
}, /*#__PURE__*/React.cloneElement(PageHeaderChild, {
minimized: minimized,
hasBackgroundImage: _this2._hasBackgroundImage()
}));
});
}
}, {
key: "_renderHeaderContainer",
value: function _renderHeaderContainer() {
var _this3 = this;
var minimized = this.state.minimized; // placeholder when header is minimized
var placeholder = /*#__PURE__*/React.createElement("div", {
key: 'placeholder',
style: {
height: "".concat(minimized ? this._getMinimizationDiff() : 0, "px")
}
});
/**
* HeaderContainer has position sticky. The `top` value is negative, in order to let
* the container scroll out of view before the minimization occurs.
*/
var top = minimized ? "-".concat(this._getMinimizationDiff(), "px") : 0;
return /*#__PURE__*/React.createElement("div", {
"data-hook": "page-header-container",
className: st(classes.pageHeaderContainer, {
minimized: minimized,
hasTail: this._hasTail()
}),
style: _defineProperty({}, vars.minimizationTop, top),
ref: function ref(_ref5) {
return _this3.headerContainerRef = _ref5;
}
}, this._renderContentHorizontalLayout({
children: [placeholder, this._renderHeader(), this._renderTail()]
}));
}
}, {
key: "_renderScrollableContainer",
value: function _renderScrollableContainer() {
var onScrollAreaChanged = this.props.scrollProps.onScrollAreaChanged;
return /*#__PURE__*/React.createElement(ScrollableContainer, {
className: st(classes.scrollableContainer, {
hasTail: this._hasTail()
}),
dataHook: "page-scrollable-content",
"data-class": "page-scrollable-content",
ref: this.scrollableContainerRef,
onScrollAreaChanged: onScrollAreaChanged,
onScrollChanged: this._handleScroll
}, /*#__PURE__*/React.createElement("div", {
"data-hook": "safari-12-13-sticky-fix"
}, this._renderScrollableBackground(), this._renderHeaderContainer(), this._renderContentContainer(), this._renderFixedFooter()));
}
}, {
key: "_hasTail",
value: function _hasTail() {
return !!this._getNamedChildren().PageTail;
}
}, {
key: "_hasHeader",
value: function _hasHeader() {
return !!this._getNamedChildren().PageHeader;
}
}, {
key: "_renderScrollableBackground",
value: function _renderScrollableBackground() {
var _this$state2 = this.state,
headerContainerHeight = _this$state2.headerContainerHeight,
tailHeight = _this$state2.tailHeight;
var backgroundHeight = "".concat(headerContainerHeight - tailHeight + (this._hasTail() ? 0 : parseInt(stVars.backgroundCoverContentPx, 10)), "px");
if (this._hasBackgroundImage()) {
return /*#__PURE__*/React.createElement("div", {
className: classes.imageBackgroundContainer,
style: {
height: backgroundHeight
},
"data-hook": "page-background-image"
}, /*#__PURE__*/React.createElement("div", {
className: classes.imageBackground,
style: {
backgroundImage: "url(".concat(this.props.backgroundImageUrl, ")")
}
}));
}
if (this._hasGradientClassName()) {
return /*#__PURE__*/React.createElement("div", {
"data-hook": "page-gradient-class-name",
className: st(classes.gradientBackground, {}, this.props.gradientClassName),
style: {
height: backgroundHeight
}
});
}
}
}, {
key: "_renderTail",
value: function _renderTail() {
var _this4 = this;
var _this$_getNamedChildr3 = this._getNamedChildren(),
PageTail = _this$_getNamedChildr3.PageTail;
var dataHook = 'page-tail';
return PageTail && /*#__PURE__*/React.createElement("div", {
"data-hook": dataHook,
key: dataHook,
className: classes.tail,
ref: function ref(r) {
return _this4.pageHeaderTailRef = r;
}
}, PageTail);
}
}, {
key: "_renderContentContainer",
value: function _renderContentContainer() {
var footerHeight = this.state.footerHeight;
var children = this.props.children;
var childrenObject = getChildrenObject(children);
var PageContent = childrenObject.PageContent,
PageFixedContent = childrenObject.PageFixedContent;
return /*#__PURE__*/React.createElement(PageContext.Provider, {
value: {
stickyStyle: {
top: "".concat(this._getMinimizedHeaderWrapperHeight() + this.state.tailHeight, "px")
}
}
}, this._renderContentHorizontalLayout({
className: classes.contentContainer,
style: {
paddingBottom: footerHeight || '48px'
},
horizontalScroll: this.props.horizontalScroll,
children: /*#__PURE__*/React.createElement("div", {
className: classes.contentFloating
}, PageFixedContent && /*#__PURE__*/React.createElement(PageSticky, {
"data-hook": "page-fixed-content"
}, /*#__PURE__*/React.cloneElement(PageFixedContent)), this._safeGetChildren(PageContent))
}));
}
}, {
key: "render",
value: function render() {
var _this5 = this;
var _this$props2 = this.props,
dataHook = _this$props2.dataHook,
className = _this$props2.className,
minWidth = _this$props2.minWidth,
zIndex = _this$props2.zIndex,
height = _this$props2.height;
return /*#__PURE__*/React.createElement("div", {
"data-hook": dataHook,
className: st(classes.root, {}, className),
style: {
zIndex: zIndex,
height: height
}
}, /*#__PURE__*/React.createElement("div", {
"data-hook": "page",
className: classes.page,
style: {
minWidth: minWidth + 2 * parseInt(stVars.pageSidePadding, 10)
},
ref: function ref(_ref6) {
return _this5.pageRef = _ref6;
}
}, this._renderScrollableContainer()));
}
/**
* Scrolls the page to a particular set of coordinates
* @param {ScrollToOptions} scrollTo { left: number, top: number, behavior: 'smooth' | 'auto' }
*/
}, {
key: "scrollTo",
value: function scrollTo(_scrollTo) {
var scrollContainer = this._getScrollContainer();
scrollContainer.scrollTo(_scrollTo);
}
}]);
return Page;
}(React.PureComponent);
_defineProperty(Page, "defaultProps", {
minWidth: parseInt(stVars.mainContainerMinWidth, 10),
maxWidth: parseInt(stVars.mainContainerMaxWidth, 10),
scrollProps: {}
});
var FixedContent = function FixedContent(props) {
return props.children;
};
FixedContent.displayName = 'Page.FixedContent';
FixedContent.propTypes = {
children: PropTypes.element.isRequired
};
Page.displayName = 'Page';
Page.Header = PageHeader;
Page.Section = PageSection;
Page.Content = Content;
Page.FixedContent = FixedContent; // TODO: deprecate, use Page.Sticky instead
Page.Tail = Tail;
Page.FixedFooter = FixedFooter;
Page.Sticky = PageSticky;
var allowedChildren = [Page.Header, Page.Section, Page.Content, Page.FixedContent, Page.Tail, Page.FixedFooter];
Page.propTypes = {
/** Applied as data-hook HTML attribute that can be used in the tests */
dataHook: PropTypes.string,
/** Background image url of the header background */
backgroundImageUrl: PropTypes.string,
/** Sets the max width of the content (Both in header and body) NOT including the page padding */
maxWidth: PropTypes.number,
/** Sets the min width of the content (Both in header and body) NOT including the page padding */
minWidth: PropTypes.number,
/** Allow the page to scroll horizontally for large width content */
horizontalScroll: PropTypes.bool,
/** Sets the height of the page (in px/vh/etc.) */
height: PropTypes.string,
/** Sets padding of the sides of the page */
sidePadding: PropTypes.number,
/** A css class to be applied to the component's root element */
className: PropTypes.string,
/** Header background color class name, allows to add a gradient to the header */
gradientClassName: PropTypes.string,
/** Will be called with the Page's scrollable content ref after page mount.
*
* **Note** - If you need this ref just for listening to scroll events on the scrollable content then use the prop
* `scrollProps = {onScrollChanged/onScrollAreaChanged}` instead according to your needs. **/
scrollableContentRef: PropTypes.func,
/** Props related to the scrollable content of the page.
*
* **onScrollAreaChanged** - A Handler for scroll area changes, will be triggered only when the user scrolls to a
* different area of the scrollable content, see signature for possible areas
* ##### Signature:
* `function({area: {y: AreaY, x: AreaX}, target: HTMLElement}) => void`
*
* `AreaY`: top | middle | bottom | none
*
* `AreaX`: start | middle | end | none (not implemented yet)
*
* **onScrollAreaChanged** - A Generic Handler for scroll changes with throttling (100ms)
* ##### Signature:
* `function({target: HTMLElement}) => void`
* */
scrollProps: PropTypes.shape(ScrollableContainerCommonProps),
/** Accepts these components as children: `Page.Header`, `Page.Tail`, `Page.Content`, `Page.FixedContent`. Order is insignificant. */
children: PropTypes.arrayOf(function (children, key) {
var child = children[key];
if (!child) {
return;
}
var allowedDisplayNames = allowedChildren.map(function (c) {
return c.displayName;
});
var childDisplayName = child.type.displayName;
if (!allowedDisplayNames.includes(childDisplayName)) {
return new Error("Page: Invalid Prop children, unknown child ".concat(child.type));
}
}).isRequired,
/** z-index of the Page */
zIndex: PropTypes.number
};
function getChildrenObject(children) {
return React.Children.toArray(children).reduce(function (acc, child) {
switch (child.type.displayName) {
case 'Page.Header':
{
acc.PageHeader = child;
break;
}
case 'Page.Section':
{
acc.Section = child;
break;
}
case 'Page.Content':
{
acc.PageContent = child;
break;
}
case 'Page.FixedContent':
{
acc.PageFixedContent = child;
break;
}
case 'Page.Tail':
{
acc.PageTail = child;
break;
}
case 'Page.FixedFooter':
{
acc.FixedFooter = child;
break;
}
default:
{
break;
}
}
return acc;
}, {});
}
export default Page;