diff --git a/babel-build.config.js b/babel-build.config.js new file mode 100644 index 000000000..a336d504f --- /dev/null +++ b/babel-build.config.js @@ -0,0 +1,16 @@ +const rootBabel = require('./babel.config'); + +// TODO: Remove this once https://github.com/storybookjs/storybook/issues/11246 is fixed +const popper2AliasRemovalPlugin = [ + 'babel-plugin-module-resolver', + { + alias: { + 'react-popper-2': 'react-popper' + } + } +]; + +module.exports = { + ...rootBabel, + plugins: [...rootBabel.plugins, popper2AliasRemovalPlugin] +}; diff --git a/devtools/buildIndexFiles.js b/devtools/buildIndexFiles.js index 779625ad2..6f368a9b9 100644 --- a/devtools/buildIndexFiles.js +++ b/devtools/buildIndexFiles.js @@ -3,6 +3,10 @@ const path = require('path'); const srcPath = path.join(__dirname, '../src'); +// Ignore errors from the browser API not being defined in node +// eslint-disable-next-line no-undefined +global.Element = undefined; + const isComponentDirectory = (source) => { const ignoredDirectories = ['utils', 'Docs']; return lstatSync(source).isDirectory() && !ignoredDirectories.some(ignored => source.includes(ignored)); diff --git a/devtools/buildVisualStories.js b/devtools/buildVisualStories.js index 269fa5a3a..3626e2c83 100644 --- a/devtools/buildVisualStories.js +++ b/devtools/buildVisualStories.js @@ -3,6 +3,10 @@ const path = require('path'); const srcPath = path.join(__dirname, '../src'); +// Ignore errors from the browser API not being defined in node +// eslint-disable-next-line no-undefined +global.Element = undefined; + const isComponentDirectory = (source) => { const ignoredDirectories = ['utils', 'Docs']; return lstatSync(source).isDirectory() && !ignoredDirectories.some(ignored => source.includes(ignored)); diff --git a/package-lock.json b/package-lock.json index 8bc359a1f..e2dbe803a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9536,6 +9536,39 @@ "babel-helper-is-void-0": "^0.4.3" } }, + "babel-plugin-module-resolver": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-module-resolver/-/babel-plugin-module-resolver-4.0.0.tgz", + "integrity": "sha512-3pdEq3PXALilSJ6dnC4wMWr0AZixHRM4utpdpBR9g5QG7B7JwWyukQv7a9hVxkbGFl+nQbrHDqqQOIBtTXTP/Q==", + "dev": true, + "requires": { + "find-babel-config": "^1.2.0", + "glob": "^7.1.6", + "pkg-up": "^3.1.0", + "reselect": "^4.0.0", + "resolve": "^1.13.1" + }, + "dependencies": { + "pkg-up": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", + "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==", + "dev": true, + "requires": { + "find-up": "^3.0.0" + } + }, + "resolve": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", + "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } + } + } + }, "babel-plugin-named-asset-import": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/babel-plugin-named-asset-import/-/babel-plugin-named-asset-import-0.3.6.tgz", @@ -14795,6 +14828,24 @@ "unpipe": "~1.0.0" } }, + "find-babel-config": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/find-babel-config/-/find-babel-config-1.2.0.tgz", + "integrity": "sha512-jB2CHJeqy6a820ssiqwrKMeyC6nNdmrcgkKWJWmpoxpE8RKciYJXCcXRq1h2AzCo5I5BJeN2tkGEO3hLTuePRA==", + "dev": true, + "requires": { + "json5": "^0.5.1", + "path-exists": "^3.0.0" + }, + "dependencies": { + "json5": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", + "dev": true + } + } + }, "find-cache-dir": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", @@ -26849,6 +26900,7 @@ "version": "npm:react-popper@2.2.3", "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.2.3.tgz", "integrity": "sha512-mOEiMNT1249js0jJvkrOjyHsGvqcJd3aGW/agkiMoZk3bZ1fXN1wQszIQSjHIai48fE67+zwF8Cs+C4fWqlfjw==", + "dev": true, "requires": { "react-fast-compare": "^3.0.1", "warning": "^4.0.2" @@ -26857,7 +26909,8 @@ "react-fast-compare": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz", - "integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==" + "integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==", + "dev": true } } }, @@ -27924,6 +27977,12 @@ "integrity": "sha1-5UBLgVV+91225JxacgBIk/4D4WI=", "dev": true }, + "reselect": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.0.0.tgz", + "integrity": "sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA==", + "dev": true + }, "resize-observer-polyfill": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", diff --git a/package.json b/package.json index 515eeea96..02ec93af4 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "scripts": { "build:index": "babel-node devtools/buildIndexFiles.js", "build:storybook": "babel-node devtools/buildVisualStories.js && babel-node scripts/copy-assets.js", - "build:cjs": "cross-env NODE_ENV=production BABEL_ENV=cjs babel src --ignore \"src/**/*.test.js\",\"src/**/*.stories.js\",\"src/Docs/*.stories.mdx\" --out-dir lib", + "build:cjs": "cross-env NODE_ENV=production BABEL_ENV=cjs babel src --config-file ./babel-build.config.js --ignore \"src/**/*.test.js\",\"src/**/*.stories.js\",\"src/Docs/*.stories.mdx\" --out-dir lib", "build": "npm run build:index && rm -rf lib && npm run build:cjs", "deploy": "gh-pages -d storybook-static", "dry-run": "npm run build && npm publish --dry-run", @@ -59,7 +59,6 @@ "react-focus-lock": "^2.3.1", "react-overlays": "^3.2.0", "react-popper": "^2.2.3", - "react-popper-2": "npm:react-popper@^2.2.3", "shortid": "^2.2.14", "tabbable": "^4.0.0" }, @@ -92,6 +91,7 @@ "babel-jest": "^24.8.0", "babel-loader": "8.1.0", "babel-plugin-client-only-require": "^1.0.1", + "babel-plugin-module-resolver": "^4.0.0", "babel-plugin-named-asset-import": "^0.3.2", "babel-plugin-require-context-hook": "^1.0.0", "babel-plugin-transform-react-remove-prop-types": "^0.4.23", @@ -123,6 +123,7 @@ "puppeteer": "^3.3.0", "react": "^16.8.0", "react-dom": "^16.8.0", + "react-popper-2": "npm:react-popper@^2.2.3", "react-router-dom": "^5.2.0", "react-syntax-highlighter": "^12.2.1", "react-test-renderer": "^16.8.0", diff --git a/src/DatePicker/__stories__/DatePicker.stories.js b/src/DatePicker/__stories__/DatePicker.stories.js index 6a0a35239..8cf6aaa70 100644 --- a/src/DatePicker/__stories__/DatePicker.stories.js +++ b/src/DatePicker/__stories__/DatePicker.stories.js @@ -3,7 +3,6 @@ import DatePicker from '../DatePicker'; import FormLabel from '../../Forms/FormLabel'; import LayoutGrid from '../../LayoutGrid/LayoutGrid'; import moment from 'moment'; -import React from 'react'; import { boolean, date, @@ -13,6 +12,7 @@ import { text, withKnobs } from '@storybook/addon-knobs'; +import React, { useEffect, useRef, useState } from 'react'; export default { title: 'Component API/DatePicker', @@ -72,6 +72,31 @@ export const compact = () => ( compact.storyName = 'Compact'; +export const withCustomFlipContainer = () => { + const containerRef = useRef(); + const [flipContainer, setFlipContainer] = useState(); + + useEffect(() => { + setFlipContainer(containerRef.current); + }); + + return ( +
+
+ +
+
+
+ ); +}; + export const disabled = () => ( ); diff --git a/src/DatePicker/__stories__/__snapshots__/DatePicker.stories.storyshot b/src/DatePicker/__stories__/__snapshots__/DatePicker.stories.storyshot index b0c2e66e4..44ec7e52c 100644 --- a/src/DatePicker/__stories__/__snapshots__/DatePicker.stories.storyshot +++ b/src/DatePicker/__stories__/__snapshots__/DatePicker.stories.storyshot @@ -1426,3 +1426,83 @@ exports[`Storyshots Component API/DatePicker Weekday Start (Monday Start) 1`] =
`; + +exports[`Storyshots Component API/DatePicker With Custom Flip Container 1`] = ` +
+
+
+
+
+
+
+
+ + +
+
+
+
+
+
+
+
+
+`; diff --git a/src/Popover/Popover.js b/src/Popover/Popover.js index c40d9893a..8499d557e 100644 --- a/src/Popover/Popover.js +++ b/src/Popover/Popover.js @@ -97,6 +97,8 @@ class Popover extends Component { disableEdgeDetection, disableKeyPressHandler, disableTriggerOnClick, + fallbackPlacements, + flipContainer, firstFocusIndex, onClickOutside, onEscapeKey, @@ -171,6 +173,8 @@ class Popover extends Component { ', () => { }); }); + describe('flip modifiers', () => { + test('disableEdgeDetection props', () => { + const popoverWithParent = mount(popOver).setProps({ disableEdgeDetection: true }); + const popperComponent = popoverWithParent.find(ReactPopper); + const flipModifier = popperComponent.props().modifiers.find(m => m.name === 'flip'); + expect(flipModifier.enabled).toBe(false); + }); + + test('disableEdgeDetection default', () => { + const popoverWithParent = mount(popOver).setProps({ }); + const popperComponent = popoverWithParent.find(ReactPopper); + const flipModifier = popperComponent.props().modifiers.find(m => m.name === 'flip'); + expect(flipModifier.enabled).not.toBeDefined(); + }); + + test('fallback placements props', () => { + const popoverWithParent = mount(popOver).setProps({ fallbackPlacements: ['top'] }); + const popperComponent = popoverWithParent.find(ReactPopper); + const flipModifier = popperComponent.props().modifiers.find(m => m.name === 'flip'); + expect(flipModifier.options.fallbackPlacements).toEqual(['top']); + }); + + test('fallback placements default', () => { + const popoverWithParent = mount(popOver).setProps({ }); + const popperComponent = popoverWithParent.find(ReactPopper); + const flipModifier = popperComponent.props().modifiers.find(m => m.name === 'flip'); + expect(flipModifier.options.fallbackPlacements).toEqual(Popper.defaultProps.fallbackPlacements); + }); + + test('flip container props', () => { + const boundary = document.createElement('div'); + const popoverWithParent = mount(popOver).setProps({ flipContainer: boundary }); + const popperComponent = popoverWithParent.find(ReactPopper); + const flipModifier = popperComponent.props().modifiers.find(m => m.name === 'flip'); + expect(flipModifier.options.boundary).toBe(boundary); + }); + + test('flip container default', () => { + const popoverWithParent = mount(popOver).setProps({ }); + const popperComponent = popoverWithParent.find(ReactPopper); + const flipModifier = popperComponent.props().modifiers.find(m => m.name === 'flip'); + expect(flipModifier.options.boundary).not.toBeDefined(); + }); + }); + describe('Callback handler', () => { test('should dispatch the onClickOutside callback with the event', () => { let f = jest.fn(); diff --git a/src/Popover/__stories__/Popover.stories.js b/src/Popover/__stories__/Popover.stories.js index 7242f16a3..0a1f55fb7 100644 --- a/src/Popover/__stories__/Popover.stories.js +++ b/src/Popover/__stories__/Popover.stories.js @@ -5,11 +5,14 @@ import Dialog from '../../Dialog/Dialog'; import Icon from '../../Icon/Icon'; import Menu from '../../Menu/Menu'; import Popover from '../Popover'; +import Popper from '../../utils/_Popper'; +import { POPPER_PLACEMENTS } from '../../utils/constants'; import { + array, boolean, select } from '@storybook/addon-knobs'; -import React, { useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; export default { title: 'Component API/Popover', @@ -205,6 +208,36 @@ export const disableEdgeDetection = () => ( ); +export const withCustomFlipContainer = () => { + const containerRef = useRef(); + const [container, setContainer] = useState(); + + useEffect(() => { + setContainer(containerRef.current); + }); + + return ( +
+
+
+ } + fallbackPlacements={['left', 'right', 'top']} + flipContainer={container} + placement='bottom' /> +
+
+ ); +}; + export const noArrow = () => ( <> { ); }; -export const dev = () => ( - } - disableEdgeDetection={boolean('disableEdgeDetection', false)} - disableKeyPressHandler={boolean('disableKeyPressHandler', false)} - disabled={boolean('disabled', false)} - noArrow={boolean('noArrow', false)} - placement={select('placement', { - 'bottom-start': 'bottom-start', - 'bottom': 'bottom', - 'bottom-end': 'bottom-end', - 'left-start': 'left-start', - 'left': 'left', - 'left-end': 'left-end', - 'right-start': 'right-start', - 'right': 'right', - 'right-end': 'right-end', - 'top-start': 'top-start', - 'top': 'top', - 'top-end': 'top-end' - })} - type={select('type', { - 'dialog': 'dialog', - 'grid': 'grid', - 'listbox': 'listbox', - 'menu': 'menu', - 'tree': 'tree' - })} - useArrowKeyNavigation={boolean('useArrowKeyNavigation', false)} - widthSizingType={select('widthSizingType', { - 'none': 'none', - 'matchTarget': 'matchTarget', - 'minTarget': 'minTarget', - 'maxTarget': 'maxTarget' - })} /> -); +export const dev = () => { + const containerRef = useRef(); + const [container, setContainer] = useState(); + + useEffect(() => { + setContainer(containerRef.current); + }); + + const useContainer = boolean('Use flip container', false); + + const popover = ( + } + disableEdgeDetection={boolean('disableEdgeDetection', false)} + disableKeyPressHandler={boolean('disableKeyPressHandler', false)} + disabled={boolean('disabled', false)} + fallbackPlacements={array('fallbackPlacements', + Popper.defaultProps.fallbackPlacements + )} + // eslint-disable-next-line no-undefined + flipContainer={useContainer ? container : undefined} + noArrow={boolean('noArrow', false)} + placement={select( + 'placement', + POPPER_PLACEMENTS.reduce((a, b) => ({ ...a, [b]: b }), {}), + 'bottom-start' + )} + type={select('type', { + 'dialog': 'dialog', + 'grid': 'grid', + 'listbox': 'listbox', + 'menu': 'menu', + 'tree': 'tree' + })} + useArrowKeyNavigation={boolean('useArrowKeyNavigation', false)} + widthSizingType={select('widthSizingType', { + 'none': 'none', + 'matchTarget': 'matchTarget', + 'minTarget': 'minTarget', + 'maxTarget': 'maxTarget' + })} /> + ); + + if (useContainer) { + return ( +
+
+
+ {popover} +
+
+ ); + } + + return ( +
+ {popover} +
+ ); +}; dev.parameters = { docs: { disable: true } }; diff --git a/src/Popover/__stories__/__snapshots__/Popover.stories.storyshot b/src/Popover/__stories__/__snapshots__/Popover.stories.storyshot index 5f46369d9..c4ac1772b 100644 --- a/src/Popover/__stories__/__snapshots__/Popover.stories.storyshot +++ b/src/Popover/__stories__/__snapshots__/Popover.stories.storyshot @@ -5,27 +5,35 @@ exports[`Storyshots Component API/Popover Dev 1`] = ` dir="ltr" >
-
@@ -814,3 +822,62 @@ exports[`Storyshots Component API/Popover Width Sizing Types 1`] = `
`; + +exports[`Storyshots Component API/Popover With Custom Flip Container 1`] = ` +
+
+
+
+
+
+
+
+
+
+
+
+
+`; diff --git a/src/utils/_Popper.js b/src/utils/_Popper.js index caa535a0c..a5365b0ed 100644 --- a/src/utils/_Popper.js +++ b/src/utils/_Popper.js @@ -17,23 +17,35 @@ const defaultModifiers = [ } }, { - name: 'preventOverflow', - options: { - boundary: [] - } + name: 'preventOverflow' }, { name: 'hide' // adds the isReferenceHidden attribute - }, - { - name: 'flip', - options: { - boundary: [] // https://github.com/popperjs/popper-core/issues/1110 - } + + } ]; -const disableEdgeDetectionModifier = { name: 'flip', enabled: false }; +const createFlipModifier = ({ + disableEdgeDetection, + fallbackPlacements, + flipContainer +}) => { + if (disableEdgeDetection) { + return { + name: 'flip', + enabled: false + }; + } + + return { + name: 'flip', + options: { + fallbackPlacements, + boundary: flipContainer + } + }; +}; const matchTargetModifier = { name: 'matchTargetModifier', @@ -128,6 +140,8 @@ class Popper extends React.Component { children, cssBlock, disableEdgeDetection, + fallbackPlacements, + flipContainer, innerRef, noArrow, onClickOutside, @@ -145,11 +159,9 @@ class Popper extends React.Component { const modifiers = [ ...defaultModifiers, + createFlipModifier({ disableEdgeDetection, fallbackPlacements, flipContainer }), ...popperModifiers ]; - if (disableEdgeDetection) { - modifiers.push(disableEdgeDetectionModifier); - } if (widthSizingType === 'matchTarget') { modifiers.push(matchTargetModifier); } @@ -235,6 +247,11 @@ Popper.propTypes = { cssBlock: PropTypes.string.isRequired, referenceComponent: PropTypes.element.isRequired, disableEdgeDetection: PropTypes.bool, + fallbackPlacements: PropTypes.arrayOf(PropTypes.oneOf(POPPER_PLACEMENTS)), + flipContainer: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.instanceOf(Element)), + PropTypes.instanceOf(Element) + ]), innerRef: PropTypes.func, noArrow: PropTypes.bool, popperClassName: PropTypes.string, @@ -252,6 +269,7 @@ Popper.propTypes = { }; Popper.defaultProps = { + fallbackPlacements: ['bottom-start', 'top-start', 'bottom-end', 'top-end'], popperModifiers: [], popperPlacement: 'bottom-start', onClickOutside: () => { }, diff --git a/src/utils/constants.js b/src/utils/constants.js index 03ddd5fea..1cfa6bea6 100644 --- a/src/utils/constants.js +++ b/src/utils/constants.js @@ -73,6 +73,9 @@ export const POPOVER_TYPES = [ ]; export const POPPER_PLACEMENTS = [ + 'auto', + 'auto-start', + 'auto-end', 'bottom-start', 'bottom', 'bottom-end', diff --git a/storybook-testing/__image_snapshots__/DatePicker-snap.png b/storybook-testing/__image_snapshots__/DatePicker-snap.png index 1d261b441..f11f52abf 100644 Binary files a/storybook-testing/__image_snapshots__/DatePicker-snap.png and b/storybook-testing/__image_snapshots__/DatePicker-snap.png differ diff --git a/storybook-testing/__image_snapshots__/Popover-snap.png b/storybook-testing/__image_snapshots__/Popover-snap.png index 9cc778552..13b8df689 100644 Binary files a/storybook-testing/__image_snapshots__/Popover-snap.png and b/storybook-testing/__image_snapshots__/Popover-snap.png differ diff --git a/storybook-testing/__image_snapshots__/Shellbar-snap.png b/storybook-testing/__image_snapshots__/Shellbar-snap.png index a6ec5ee53..a70c38542 100644 Binary files a/storybook-testing/__image_snapshots__/Shellbar-snap.png and b/storybook-testing/__image_snapshots__/Shellbar-snap.png differ