From 73a35d61a360d8680e7703472cf5abe6f194acfd Mon Sep 17 00:00:00 2001 From: Ed Leeks Date: Tue, 23 Feb 2021 16:45:28 +0000 Subject: [PATCH 1/9] feat(tooltip): add support for overriding default flip placements Adds `flipOverrides` prop to support passing an array of placements to override the default flip behaviour: the order of the array sets the precedence of these overrides. See `popper` docs for more information https://popper.js.org/docs/v2/modifiers/flip/#fallbackplacements --- src/components/tooltip/tooltip.component.js | 33 ++++++++++++++++- src/components/tooltip/tooltip.d.ts | 4 +- src/components/tooltip/tooltip.spec.js | 31 +++++++++++++--- src/components/tooltip/tooltip.stories.js | 20 +++++++++- src/components/tooltip/tooltip.stories.mdx | 37 +++++++++++++++++++ src/components/tooltip/tooltip.style.js | 6 +-- .../options-helper/options-helper.d.ts | 6 ++- 7 files changed, 123 insertions(+), 14 deletions(-) diff --git a/src/components/tooltip/tooltip.component.js b/src/components/tooltip/tooltip.component.js index d320c3d750..16ee1cb7e0 100644 --- a/src/components/tooltip/tooltip.component.js +++ b/src/components/tooltip/tooltip.component.js @@ -21,6 +21,7 @@ const Tooltip = React.forwardRef( id, bgColor, fontColor, + flipOverrides, ...rest }, ref @@ -67,6 +68,18 @@ const Tooltip = React.forwardRef( delay={TOOLTIP_DELAY} {...(isVisible !== undefined && { visible: isVisible })} render={(attrs) => tooltip(attrs, message)} + {...(flipOverrides && { + popperOptions: { + modifiers: [ + { + name: "flip", + options: { + fallbackPlacements: flipOverrides, + }, + }, + ], + }, + })} > {children} @@ -74,6 +87,8 @@ const Tooltip = React.forwardRef( } ); +const placements = ["top", "bottom", "left", "right"]; + Tooltip.propTypes = { /** The message to be displayed within the tooltip */ message: PropTypes.node.isRequired, @@ -82,7 +97,7 @@ Tooltip.propTypes = { /** Whether to to show the Tooltip */ isVisible: PropTypes.bool, /** Sets position of the tooltip */ - position: PropTypes.oneOf(["top", "bottom", "left", "right"]), + position: PropTypes.oneOf(placements), /** Defines the message type */ type: PropTypes.string, /** Children elements */ @@ -97,6 +112,22 @@ Tooltip.propTypes = { isPartOfInput: PropTypes.bool, /** @ignore @private */ inputSize: PropTypes.oneOf(["small", "medium", "large"]), + /** Overrides the default flip behaviour of the Tooltip, must be an array containing some or all of ["top", "bottom", "left", "right"] (see https://popper.js.org/docs/v2/modifiers/flip/#fallbackplacements) */ + flipOverrides: (props, propName) => { + const prop = props[propName]; + const isValid = + prop && + Array.isArray(prop) && + prop.every((placement) => placements.includes(placement)); + + if (!prop || isValid) { + return null; + } + return new Error( + // eslint-disable-next-line max-len + `The \`${propName}\` prop supplied to \`Tooltip\` must be an array containing some or all of ["top", "bottom", "left", "right"].` + ); + }, }; export default Tooltip; diff --git a/src/components/tooltip/tooltip.d.ts b/src/components/tooltip/tooltip.d.ts index ca49c63352..986f4b0c02 100644 --- a/src/components/tooltip/tooltip.d.ts +++ b/src/components/tooltip/tooltip.d.ts @@ -1,4 +1,5 @@ import * as React from 'react'; +import { Positions } from "../../utils/helpers/options-helper"; export interface TooltipProps { /** The message to be displayed within the tooltip */ @@ -8,7 +9,7 @@ export interface TooltipProps { /** Whether to to show the Tooltip */ isVisible?: boolean; /** Sets position of the tooltip */ - position?: 'top' | 'bottom' | 'left' | 'right'; + position?: Positions; /** Defines the message type */ type?: string; /** Children elements */ @@ -19,6 +20,7 @@ export interface TooltipProps { inputSize?: 'small' | 'medium' | 'large'; bgColor?: string; fontColor?: string; + flipOverrides?: Positions[]; } declare const Tooltip: React.FunctionComponent; diff --git a/src/components/tooltip/tooltip.spec.js b/src/components/tooltip/tooltip.spec.js index b0c22cc26c..e7819cf6be 100644 --- a/src/components/tooltip/tooltip.spec.js +++ b/src/components/tooltip/tooltip.spec.js @@ -131,11 +131,12 @@ describe("Tooltip", () => { }); describe("when the tooltip targets a component that is a part of an input", () => { - const offsets = { + const isTopOrBottom = (position) => ["top", "bottom"].includes(position); + const offsets = (position) => ({ small: "5px", - medium: "1px", - large: "-3px", - }; + medium: isTopOrBottom(position) ? "4px" : "2px", + large: isTopOrBottom(position) ? "0px" : "-2px", + }); describe.each(positions)("and position is %s", (position) => { it.each(["small", "medium", "large"])( @@ -143,7 +144,7 @@ describe("Tooltip", () => { (size) => { assertStyleMatch( { - [position]: offsets[size], + [position]: offsets(position)[size], }, render({ position, isPartOfInput: true, inputSize: size }).find( StyledTooltipWrapper @@ -249,4 +250,24 @@ describe("Tooltip", () => { }); }); }); + + describe("flipOverrides", () => { + it("does not throw an error if a valid array is passed", () => { + jest.spyOn(global.console, "error").mockImplementation(() => {}); + render({ flipOverrides: ["top", "bottom"] }); + + // eslint-disable-next-line no-console + expect(console.error).not.toHaveBeenCalled(); + global.console.error.mockReset(); + }); + + it("throws an error if a invalid array is passed", () => { + jest.spyOn(global.console, "error").mockImplementation(() => {}); + render({ flipOverrides: ["foo", "bar"] }); + + // eslint-disable-next-line no-console + expect(console.error).toHaveBeenCalled(); + global.console.error.mockReset(); + }); + }); }); diff --git a/src/components/tooltip/tooltip.stories.js b/src/components/tooltip/tooltip.stories.js index ad5645c084..3b07819719 100644 --- a/src/components/tooltip/tooltip.stories.js +++ b/src/components/tooltip/tooltip.stories.js @@ -18,17 +18,27 @@ export default { }; const props = () => { + const position = select("position", OptionsHelper.positions, "top"); + const isVertical = ["top", "bottom"].includes(position); + const enableFlipOverrides = boolean("enable flip overrides", false); + return { isVisible: boolean("isVisible", true), message: text( "message", "I'm a helpful tooltip that can display additional information to a user." ), - position: select("position", OptionsHelper.positions, "top"), + position, type: select("type", ["error", "default"], "default"), size: select("size", ["medium", "large"], "medium"), bgColor: text("bgColor", undefined), fontColor: text("fontColor", undefined), + flipOverrides: enableFlipOverrides + ? select( + "flipOverrides", + isVertical ? ["left", "right"] : ["top", "bottom"] + ) + : undefined, }; }; @@ -36,6 +46,8 @@ export const Default = () => { const { isVisible } = props(); const [stateVisible, setStateVisible] = useState(undefined); + const { flipOverrides } = props(); + return (
{ overflow: "auto", }} > - +
+### Overriding the default flip behaviour +By default if there is no room for the Tooltip to display in the given position it will attempt to flip and dynamically +place itself in the opposite position. You can pass an array of `flipOverrides` which will alter the behaviour from the +default, the Tooltip will flip to the positions defined in the array in the order they are defined. The overriding +can be seen by adjusting the window size (ideally in the canvas tab) and scrolling. The example should render the +Tooltip "bottom" intitially, then flip to "right" when there is not enough room below, and finally to the "left" position +when there is no space to render to the right anymore. + + + + {() => { + const Component = forwardRef(({ children }, ref) => ( + + )) + return ( +
+ + target + +
+ ); + }} +
+
+ ### Large tooltip diff --git a/src/components/tooltip/tooltip.style.js b/src/components/tooltip/tooltip.style.js index 5a66ba3801..5f9f84ca52 100644 --- a/src/components/tooltip/tooltip.style.js +++ b/src/components/tooltip/tooltip.style.js @@ -28,19 +28,19 @@ const tooltipOffset = (position, inputSize, isPartOfInput) => { return ` ${position}: 5px; @-moz-document url-prefix() { - ${position}: 7px; + ${position}: ${["top", "bottom"].includes(position) ? "7px" : "6px"}; } `; case "large": return ` - ${position}: -3px; + ${position}: ${["top", "bottom"].includes(position) ? "0px" : "-2px"}; @-moz-document url-prefix() { ${position}: -1px; } `; default: return ` - ${position}: 1px; + ${position}: ${["top", "bottom"].includes(position) ? "4px" : "2px"}; @-moz-document url-prefix() { ${position}: 4px; } diff --git a/src/utils/helpers/options-helper/options-helper.d.ts b/src/utils/helpers/options-helper/options-helper.d.ts index 21e2c1e85d..ec497f0c45 100644 --- a/src/utils/helpers/options-helper/options-helper.d.ts +++ b/src/utils/helpers/options-helper/options-helper.d.ts @@ -294,6 +294,8 @@ export type SizesType = 'small' | 'large'; export type ThemesBinary = 'primary' | 'secondary'; +export type Positions = "top" | "bottom" | "left" | "right"; + export interface MarginSpacingProps { m?: number | string; mt?: number | string; @@ -343,13 +345,13 @@ export interface FlexBoxProps { alignContent?: string; justifyItems?: string; justifyContent?: string; - felxWrap?: string; + flexWrap?: string; flexDirection?: string; flex?: string; flexGrow?: number; flexShrink?: number; flexBasis?: string; - justofySelf?: string; + justifySelf?: string; alignSelf?: string; order?: number; } From 47dc4244acdae1b53ca4ad4857803d51ab831bd9 Mon Sep 17 00:00:00 2001 From: Ed Leeks Date: Wed, 24 Feb 2021 09:23:00 +0000 Subject: [PATCH 2/9] feat(icon): add support for tooltip flip placement overrides Adds `tooltipFlipOverrides` to `Icon` to support overriding the default flip behaviour of `Tooltip`. --- src/components/icon/icon.component.js | 22 ++++++++++- src/components/icon/icon.d.ts | 6 ++- src/components/icon/icon.spec.js | 54 ++++++++++++++++++++++++++- src/components/icon/icon.stories.js | 43 ++++++++++++++++++--- src/components/icon/icon.stories.mdx | 22 +++-------- 5 files changed, 120 insertions(+), 27 deletions(-) diff --git a/src/components/icon/icon.component.js b/src/components/icon/icon.component.js index fb3b1f5b74..18a2924ecf 100644 --- a/src/components/icon/icon.component.js +++ b/src/components/icon/icon.component.js @@ -24,6 +24,7 @@ const Icon = React.forwardRef( tooltipVisible, tooltipBgColor, tooltipFontColor, + tooltipFlipOverrides, tabIndex, isPartOfInput, inputSize, @@ -90,6 +91,7 @@ const Icon = React.forwardRef( inputSize={inputSize} bgColor={tooltipBgColor} fontColor={tooltipFontColor} + flipOverrides={tooltipFlipOverrides} > {icon} @@ -100,6 +102,8 @@ const Icon = React.forwardRef( } ); +const placements = ["top", "bottom", "left", "right"]; + Icon.propTypes = { /** * @private @@ -146,13 +150,29 @@ Icon.propTypes = { /** The message string to be displayed in the tooltip */ tooltipMessage: PropTypes.string, /** The position to display the tooltip */ - tooltipPosition: PropTypes.oneOf(["top", "bottom", "left", "right"]), + tooltipPosition: PropTypes.oneOf(placements), /** Control whether the tooltip is visible */ tooltipVisible: PropTypes.bool, /** Override background color of the Tooltip, provide any color from palette or any valid css color value. */ tooltipBgColor: PropTypes.string, /** Override font color of the Tooltip, provide any color from palette or any valid css color value. */ tooltipFontColor: PropTypes.string, + /** Overrides the default flip behaviour of the Tooltip, must be an array containing some or all of ["top", "bottom", "left", "right"] (see https://popper.js.org/docs/v2/modifiers/flip/#fallbackplacements) */ + tooltipFlipOverrides: (props, propName) => { + const prop = props[propName]; + const isValid = + prop && + Array.isArray(prop) && + prop.every((placement) => placements.includes(placement)); + + if (!prop || isValid) { + return null; + } + return new Error( + // eslint-disable-next-line max-len + `The \`${propName}\` prop supplied to \`Icon\` must be an array containing some or all of ["top", "bottom", "left", "right"].` + ); + }, /** @ignore @private */ isPartOfInput: PropTypes.bool, /** @ignore @private */ diff --git a/src/components/icon/icon.d.ts b/src/components/icon/icon.d.ts index 9ca759f498..506be3f694 100644 --- a/src/components/icon/icon.d.ts +++ b/src/components/icon/icon.d.ts @@ -1,5 +1,5 @@ import * as React from 'react'; - +import { Positions } from "../../utils/helpers/options-helper"; export interface IconProps { /** Icon type */ type: string; @@ -28,13 +28,15 @@ export interface IconProps { /** The message string to be displayed in the tooltip */ tooltipMessage?: string; /** The position to display the tooltip */ - tooltipPosition?: "top" | "bottom" | "left" | "right"; + tooltipPosition?: Positions; /** Control whether the tooltip is visible */ tooltipVisible?: boolean; /** Override background color of the Tooltip, provide any color from palette or any valid css color value. */ tooltipBgColor?: string; /** Override font color of the Tooltip, provide any color from palette or any valid css color value. */ tooltipFontColor?: string; + /** Overrides the default flip behaviour of the Tooltip */ + tooltipFlipOverrides?: Positions[]; } declare const Icon: React.ComponentType; diff --git a/src/components/icon/icon.spec.js b/src/components/icon/icon.spec.js index 4eac92ac87..5496c802a2 100644 --- a/src/components/icon/icon.spec.js +++ b/src/components/icon/icon.spec.js @@ -177,16 +177,31 @@ describe("Icon component", () => { const wrongColors = ["rgb(0,0)", "#ff", "test"]; describe.each(wrongColors)("when wrong color prop is provided", (color) => { + beforeEach(() => { + jest.spyOn(global.console, "error").mockImplementation(() => {}); + }); + + afterEach(() => { + global.console.error.mockReset(); + }); + it("throws an error", () => { - jest.spyOn(global.console, "error"); mount(); // eslint-disable-next-line no-console expect(console.error).toHaveBeenCalled(); }); }); + describe.each(wrongColors)("when wrong bg prop is provided", (color) => { + beforeEach(() => { + jest.spyOn(global.console, "error").mockImplementation(() => {}); + }); + + afterEach(() => { + global.console.error.mockReset(); + }); + it("throws an error", () => { - jest.spyOn(global.console, "error"); mount(); // eslint-disable-next-line no-console expect(console.error).toHaveBeenCalled(); @@ -499,5 +514,40 @@ describe("Icon component", () => { expect(wrapper.find(Tooltip).props().isVisible).toEqual(false); }); + + describe("tooltipFlipOverrides", () => { + it("does not throw an error if a valid array is passed", () => { + global.console.error.mockReset(); + + jest.spyOn(global.console, "error").mockImplementation(() => {}); + + mount( + + ); + + // eslint-disable-next-line no-console + expect(console.error).not.toHaveBeenCalled(); + global.console.error.mockReset(); + }); + + it("throws an error if a invalid array is passed", () => { + jest.spyOn(global.console, "error").mockImplementation(() => {}); + mount( + + ); + + // eslint-disable-next-line no-console + expect(console.error).toHaveBeenCalled(); + global.console.error.mockReset(); + }); + }); }); }); diff --git a/src/components/icon/icon.stories.js b/src/components/icon/icon.stories.js index 9d06ee4573..3697486d85 100644 --- a/src/components/icon/icon.stories.js +++ b/src/components/icon/icon.stories.js @@ -20,14 +20,33 @@ export default { const commonKnobs = () => { const tooltipMessage = text("tooltipMessage", ""); + const tooltipPosition = tooltipMessage + ? select("position", OptionsHelper.positions, "top") + : undefined; + + const isVertical = ["top", "bottom"].includes(tooltipPosition); + const enableFlipOverrides = tooltipMessage + ? boolean("enable flip overrides", false) + : undefined; + return { tooltipMessage, type: select("type", OptionsHelper.icons, "add"), - tooltipPosition: tooltipMessage - ? select("tooltipPosition", OptionsHelper.positions, "top") + tooltipPosition, + tooltipBgColor: tooltipMessage + ? text("tooltipBgColor", undefined) + : undefined, + tooltipFontColor: tooltipMessage + ? text("tooltipFontColor", undefined) : undefined, - tooltipBgColor: text("tooltipBgColor", undefined), - tooltipFontColor: text("tooltipFontColor", undefined), + tooltipFlipOverrides: + tooltipMessage && enableFlipOverrides + ? select( + "tooltipFlipOverrides", + isVertical ? ["left", "right"] : ["top", "bottom"], + isVertical ? "right" : "bottom" + ) + : undefined, }; }; @@ -69,7 +88,21 @@ export const Default = () => { if (knobs.iconColor === "on-dark-background") knobs.bgTheme = "info"; - return ; + const { tooltipFlipOverrides } = commonKnobs(); + + const flipOverrides = tooltipFlipOverrides + ? [tooltipFlipOverrides] + : undefined; + + return ( +
+ +
+ ); }; export const All = () => ( diff --git a/src/components/icon/icon.stories.mdx b/src/components/icon/icon.stories.mdx index 7e3681b75d..445df0d9f2 100644 --- a/src/components/icon/icon.stories.mdx +++ b/src/components/icon/icon.stories.mdx @@ -46,28 +46,16 @@ const MyComponent = () => (
### With tooltip - -The Icon supports rendering tooltips internally, just pass a string to `tooltipMessage` to enable this feature. Other props, -such as `tooltipPosition`, `tooltipVisible` , `tooltipBgColor` and `tooltipFontColor` are also surfaced. +The Icon supports rendering tooltips internally, just pass a string to `tooltipMessage` to enable this feature. Other props, +such as `tooltipPosition`, `tooltipVisible` , `tooltipBgColor`, `tooltipFontColor` and `tooltipFlipOverrides` are also surfaced.
- - + + +
From 0471c5b612a37f42045e6f7237559ee81b28f5ae Mon Sep 17 00:00:00 2001 From: Ed Leeks Date: Wed, 24 Feb 2021 17:14:29 +0000 Subject: [PATCH 3/9] feat(validation-icon): add support for tooltip flip placement overrides Adds `tooltipFlipOverrides` to `ValidationIcon` to support overriding the default flip behaviour of `Tooltip` --- .../validations/validation-icon.component.js | 22 +++++++ .../validations/validation-icon.spec.js | 57 +++++++++++++++++-- 2 files changed, 75 insertions(+), 4 deletions(-) diff --git a/src/components/validations/validation-icon.component.js b/src/components/validations/validation-icon.component.js index ab92a436f6..a44a127347 100644 --- a/src/components/validations/validation-icon.component.js +++ b/src/components/validations/validation-icon.component.js @@ -26,6 +26,7 @@ const ValidationIcon = ({ tabIndex, onClick, tooltipPosition, + tooltipFlipOverrides, }) => { const { hasFocus, hasMouseOver } = useContext(InputContext); const { @@ -67,6 +68,11 @@ const ValidationIcon = ({ groupHasMouseOver || triggeredByIcon } + tooltipFlipOverrides={ + isPartOfInput && !tooltipFlipOverrides + ? ["top", "bottom"] + : tooltipFlipOverrides + } isPartOfInput={isPartOfInput} inputSize={size} /> @@ -81,6 +87,22 @@ ValidationIcon.propTypes = { iconId: PropTypes.string, /** Define position of the tooltip */ tooltipPosition: PropTypes.string, + /** Overrides the default flip behaviour of the Tooltip, must be an array containing some or all of ["top", "bottom", "left", "right"] (see https://popper.js.org/docs/v2/modifiers/flip/#fallbackplacements) */ + tooltipFlipOverrides: (props, propName, componentName) => { + const prop = props[propName]; + const isValid = + prop && + Array.isArray(prop) && + prop.every((placement) => OptionsHelper.positions.includes(placement)); + + if (!prop || isValid) { + return null; + } + return new Error( + // eslint-disable-next-line max-len + `The \`${propName}\` prop supplied to \`${componentName}\` must be an array containing some or all of ["top", "bottom", "left", "right"].` + ); + }, /** An onClick handler */ onClick: PropTypes.func, /** A boolean to indicate if the icon is part of an input */ diff --git a/src/components/validations/validation-icon.spec.js b/src/components/validations/validation-icon.spec.js index 6b70b55528..90b6a731a8 100644 --- a/src/components/validations/validation-icon.spec.js +++ b/src/components/validations/validation-icon.spec.js @@ -65,31 +65,80 @@ describe("ValidationIcon", () => { expect(tooltip.props().isVisible).toEqual(true); }); - it("shows the Tooltip if the Help component has mouse over event", () => { + it("shows the Tooltip if component has mouse over event", () => { const wrapper = mount(); wrapper.simulate("mouseover"); expect(wrapper.find(Tooltip).props().isVisible).toEqual(true); }); - it("hides the Tooltip if the Help component has mouse leave event", () => { + it("hides the Tooltip if component has mouse leave event", () => { const wrapper = mount(); wrapper.simulate("mouseover"); wrapper.simulate("mouseleave"); expect(wrapper.find(Tooltip).props().isVisible).toEqual(false); }); - it("shows the Tooltip if the Help component has focus event", () => { + it("shows the Tooltip if component has focus event", () => { const wrapper = mount(); wrapper.simulate("focus"); expect(wrapper.find(Tooltip).props().isVisible).toEqual(true); }); - it("hides the Tooltip if the Help component has blur event", () => { + it("hides the Tooltip if component has blur event", () => { const wrapper = mount(); wrapper.simulate("focus"); wrapper.simulate("blur"); expect(wrapper.find(Tooltip).props().isVisible).toEqual(false); }); + + describe("tooltipFlipOverrides", () => { + it("sets Tooltip to flip `top` and `bottom` when `isPartOfInput` and no `tooltipFlipOverrides` set", () => { + const wrapper = mount(); + expect(wrapper.find(Tooltip).props().flipOverrides).toEqual([ + "top", + "bottom", + ]); + }); + + it("sets Tooltip to flip to given placements defined by `tooltipFlipOverrides`", () => { + const wrapper = mount( + + ); + expect(wrapper.find(Tooltip).props().flipOverrides).toEqual([ + "left", + "top", + ]); + }); + + it("does not throw an error if a valid array is passed", () => { + jest.spyOn(global.console, "error").mockImplementation(() => {}); + mount( + + ); + + // eslint-disable-next-line no-console + expect(console.error).not.toHaveBeenCalled(); + global.console.error.mockReset(); + }); + + it("throws an error if a invalid array is passed", () => { + jest.spyOn(global.console, "error").mockImplementation(() => {}); + mount( + + ); + + // eslint-disable-next-line no-console + expect(console.error).toHaveBeenCalled(); + global.console.error.mockReset(); + }); + }); }); function renderWithInputContext( From 2c014998f6166556d5e937ee8d5c3c4a37ae5267 Mon Sep 17 00:00:00 2001 From: Ed Leeks Date: Mon, 1 Mar 2021 13:37:50 +0000 Subject: [PATCH 4/9] feat(help): add support for tooltip flip placement overrides Adds `tooltipFlipOverrides` to `Help` to support overriding the default flip behaviour of `Tooltip`. --- src/components/help/help.component.js | 18 ++++++++++++++++ src/components/help/help.d.ts | 5 +++-- src/components/help/help.spec.js | 25 +++++++++++++++++++++ src/components/help/help.stories.js | 31 +++++++++++++++++++++++---- 4 files changed, 73 insertions(+), 6 deletions(-) diff --git a/src/components/help/help.component.js b/src/components/help/help.component.js index 9c03c383b2..8c358de160 100644 --- a/src/components/help/help.component.js +++ b/src/components/help/help.component.js @@ -22,6 +22,7 @@ const Help = (props) => { type, tooltipBgColor, tooltipFontColor, + tooltipFlipOverrides, } = props; useEffect(() => { @@ -76,6 +77,7 @@ const Help = (props) => { tooltipVisible={isFocused || isTooltipVisible} tooltipBgColor={tooltipBgColor} tooltipFontColor={tooltipFontColor} + tooltipFlipOverrides={tooltipFlipOverrides} /> ); @@ -104,6 +106,22 @@ Help.propTypes = { tooltipBgColor: PropTypes.string, /** Override font color of the Tooltip, provide any color from palette or any valid css color value. */ tooltipFontColor: PropTypes.string, + /** Overrides the default flip behaviour of the Tooltip, must be an array containing some or all of ["top", "bottom", "left", "right"] (see https://popper.js.org/docs/v2/modifiers/flip/#fallbackplacements) */ + tooltipFlipOverrides: (props, propName, componentName) => { + const prop = props[propName]; + const isValid = + prop && + Array.isArray(prop) && + prop.every((placement) => OptionsHelper.positions.includes(placement)); + + if (!prop || isValid) { + return null; + } + return new Error( + // eslint-disable-next-line max-len + `The \`${propName}\` prop supplied to \`${componentName}\` must be an array containing some or all of ["top", "bottom", "left", "right"].` + ); + }, }; Help.defaultProps = { diff --git a/src/components/help/help.d.ts b/src/components/help/help.d.ts index 9c8da71561..4b97653a5f 100644 --- a/src/components/help/help.d.ts +++ b/src/components/help/help.d.ts @@ -1,5 +1,5 @@ import * as React from 'react'; -import { IconTypes } from "../../utils/helpers/options-helper/options-helper"; +import { IconTypes, Positions } from "../../utils/helpers/options-helper/options-helper"; export interface HelpProps { className?: string; @@ -10,9 +10,10 @@ export interface HelpProps { href?: string; isFocused?: boolean; type?: IconTypes; - tooltipPosition?: "top" | "bottom" | "left" | "right"; + tooltipPosition?: Positions; tooltipBgColor?: string; tooltipFontColor?: string; + tooltipFlipOverrides?: Positions[]; } declare const Help: React.ComponentType; diff --git a/src/components/help/help.spec.js b/src/components/help/help.spec.js index 9e55fa445f..a6a19accfc 100644 --- a/src/components/help/help.spec.js +++ b/src/components/help/help.spec.js @@ -197,6 +197,31 @@ describe("Help", () => { document.body.removeChild(domNode); }); }); + + describe("tooltipFlipOverrides", () => { + it("does not throw an error if a valid array is passed", () => { + jest.spyOn(global.console, "error").mockImplementation(() => {}); + + renderHelp( + { type: "home", tooltipFlipOverrides: ["top", "bottom"] }, + mount + ); + + // eslint-disable-next-line no-console + expect(console.error).not.toHaveBeenCalled(); + global.console.error.mockReset(); + }); + + it("throws an error if a invalid array is passed", () => { + jest.spyOn(global.console, "error").mockImplementation(() => {}); + + renderHelp({ type: "home", tooltipFlipOverrides: ["foo", "bar"] }, mount); + + // eslint-disable-next-line no-console + expect(console.error).toHaveBeenCalled(); + global.console.error.mockReset(); + }); + }); }); function renderHelp(props, renderer = shallow) { diff --git a/src/components/help/help.stories.js b/src/components/help/help.stories.js index 98b40fcf35..e95330fce5 100644 --- a/src/components/help/help.stories.js +++ b/src/components/help/help.stories.js @@ -1,5 +1,5 @@ import React from "react"; -import { text, select } from "@storybook/addon-knobs"; +import { text, select, boolean } from "@storybook/addon-knobs"; import OptionsHelper from "../../utils/helpers/options-helper"; import Help from "./help.component"; @@ -28,17 +28,40 @@ export const Default = () => { : undefined; const href = text("href", "http://www.sage.com"); const type = select("type", OptionsHelper.icons, "help"); - const tooltipBgColor = text("tooltipBgColor", undefined); - const tooltipFontColor = text("tooltipFontColor", undefined); + const tooltipBgColor = children + ? text("tooltipBgColor", undefined) + : undefined; + const tooltipFontColor = children + ? text("tooltipFontColor", undefined) + : undefined; + + const isVertical = ["top", "bottom"].includes(tooltipPosition); + const enableFlipOverrides = children + ? boolean("enable flip overrides", false) + : undefined; + + const tooltipFlipOverrides = + children && enableFlipOverrides + ? select( + "tooltipFlipOverrides", + isVertical ? ["left", "right"] : ["top", "bottom"], + isVertical ? "right" : "bottom" + ) + : undefined; + + const flipOverrides = tooltipFlipOverrides + ? [tooltipFlipOverrides] + : undefined; return ( -
+
{children} From 79d5f239069fa771543802deb5a9a1b2f58ab6be Mon Sep 17 00:00:00 2001 From: Ed Leeks Date: Mon, 1 Mar 2021 14:42:58 +0000 Subject: [PATCH 5/9] feat(button-toggle-group): add support for overriding tooltip flip behaviour Sets `Tooltip` to flip "top" or " bottom" if `validationOnLabel` is false --- .../button-toggle-group/button-toggle-group.component.js | 7 ++++++- .../button-toggle-group/button-toggle-group.spec.js | 8 ++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/components/button-toggle-group/button-toggle-group.component.js b/src/components/button-toggle-group/button-toggle-group.component.js index 88935fd271..1ceded9d28 100644 --- a/src/components/button-toggle-group/button-toggle-group.component.js +++ b/src/components/button-toggle-group/button-toggle-group.component.js @@ -44,7 +44,12 @@ const BaseButtonToggleGroup = (props) => { > {children} - {!validationOnLabel && } + {!validationOnLabel && ( + + )} diff --git a/src/components/button-toggle-group/button-toggle-group.spec.js b/src/components/button-toggle-group/button-toggle-group.spec.js index b8bb308beb..e2d24f0546 100644 --- a/src/components/button-toggle-group/button-toggle-group.spec.js +++ b/src/components/button-toggle-group/button-toggle-group.spec.js @@ -8,6 +8,7 @@ import { baseTheme, mintTheme } from "../../style/themes"; import { assertStyleMatch } from "../../__spec_helper__/test-utils"; import { StyledButtonToggleLabel } from "../button-toggle/button-toggle.style"; import StyledValidationIcon from "../validations/validation-icon.style"; +import ValidationIcon from "../validations"; import Label from "../../__experimental__/components/label"; import ButtonToggleGroup from "./button-toggle-group.component"; @@ -78,6 +79,7 @@ describe("ButtonToggleGroup", () => { expect(wrapper).toMatchSnapshot(); }); }); + describe("Style props", () => { it("renders with the correct width", () => { const wrapper = renderWithTheme( @@ -117,6 +119,7 @@ describe("ButtonToggleGroup", () => { { modifier: `${StyledButtonToggleLabel}` } ); }); + it("renders validation icon on input", () => { const wrapper = renderWithTheme( { theme: baseTheme, [type]: "Message" }, @@ -128,7 +131,12 @@ describe("ButtonToggleGroup", () => { .find(StyledValidationIcon) .exists() ).toBe(true); + + expect( + wrapper.find(ValidationIcon).props().tooltipFlipOverrides + ).toEqual(["top", "bottom"]); }); + it("renders validation icon on label when validationOnLabel passed as true", () => { const wrapper = renderWithTheme( { From 224d99d8747c03a5e558118a11a07771fb4e9257 Mon Sep 17 00:00:00 2001 From: Ed Leeks Date: Wed, 10 Mar 2021 15:30:05 +0000 Subject: [PATCH 6/9] feat(simple-color-picker): add tooltip flip overrides to validation icon Pass tooltip flip override to `ValidationICon` when it is rendered on input --- .../simple-color-picker/simple-color-picker.component.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/__experimental__/components/simple-color-picker/simple-color-picker.component.js b/src/__experimental__/components/simple-color-picker/simple-color-picker.component.js index ece17fdf5a..baf5a66c1b 100644 --- a/src/__experimental__/components/simple-color-picker/simple-color-picker.component.js +++ b/src/__experimental__/components/simple-color-picker/simple-color-picker.component.js @@ -228,7 +228,12 @@ const SimpleColorPicker = (props) => { )} - {!validationOnLegend && } + {!validationOnLegend && ( + + )} ); From 2abfbd44528274f8363f1ee1c684f40fc76947e3 Mon Sep 17 00:00:00 2001 From: Ed Leeks Date: Wed, 10 Mar 2021 15:32:01 +0000 Subject: [PATCH 7/9] feat(fieldset): add tooltip flip override to validation icon rendered on labels Passes tooltip flip override when `ValidationIcon` is rendered on label in `FieldSet` --- src/__internal__/fieldset/fieldset.component.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/__internal__/fieldset/fieldset.component.js b/src/__internal__/fieldset/fieldset.component.js index eb5940ce46..17e4d496cf 100644 --- a/src/__internal__/fieldset/fieldset.component.js +++ b/src/__internal__/fieldset/fieldset.component.js @@ -50,7 +50,12 @@ const Fieldset = ({ )} - + )} {children} From ba6b73a074648f0558536962352b629b7727e2d8 Mon Sep 17 00:00:00 2001 From: Ed Leeks Date: Wed, 10 Mar 2021 15:57:33 +0000 Subject: [PATCH 8/9] feat(switch): add tooltip flip override to validation icon rendered on input Passed tooltip flip overrides to `ValidationIcon` when rendered on input instead of label --- .../components/switch/switch-slider.component.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/__experimental__/components/switch/switch-slider.component.js b/src/__experimental__/components/switch/switch-slider.component.js index 0a44e73bd8..0844813c32 100644 --- a/src/__experimental__/components/switch/switch-slider.component.js +++ b/src/__experimental__/components/switch/switch-slider.component.js @@ -61,6 +61,7 @@ const SwitchSlider = (props) => { warning={warning} info={info} size={props.size} + tooltipFlipOverrides={["top", "bottom"]} /> )} From 2b88b25242f5862584ff03b6aaa832537b8267ec Mon Sep 17 00:00:00 2001 From: Ed Leeks Date: Wed, 10 Mar 2021 16:00:07 +0000 Subject: [PATCH 9/9] feat(label): add tooltip flip override to validation icon when rendered Passes tooltip flip override to `ValidationIcon` rendered on the `Label` --- src/__experimental__/components/label/label.component.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/__experimental__/components/label/label.component.js b/src/__experimental__/components/label/label.component.js index 5eb6fa8162..6e680b83f7 100644 --- a/src/__experimental__/components/label/label.component.js +++ b/src/__experimental__/components/label/label.component.js @@ -98,6 +98,7 @@ const Label = ({ warning={warning} info={info} tooltipPosition={tooltipPositionValue} + tooltipFlipOverrides={["top", "bottom"]} /> );