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

fix(button): mouse out #9425

Closed
wants to merge 11 commits into from
104 changes: 103 additions & 1 deletion packages/react/src/components/Button/Button-story.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/

import React from 'react';
import React, { useState } from 'react';
import { action } from '@storybook/addon-actions';
import { withKnobs, boolean, select, text } from '@storybook/addon-knobs';
import { iconAddSolid, iconSearch } from 'carbon-icons';
Expand Down Expand Up @@ -163,6 +163,69 @@ _Default.story = {
name: 'Button',
};

export const MouseOut = () => {
const [timer, setTimer] = useState(0);
const simulateLoading = () => {
setTimer(true);
setTimeout(() => {
setTimer(false);
}, 3000);
};

const [timer2, setTimer2] = useState(0);
const simulateLoading2 = () => {
setTimer2(true);
setTimeout(() => {
setTimer2(false);
}, 3000);
};

const [timer3, setTimer3] = useState(0);
const simulateLoading3 = () => {
setTimer3(true);
setTimeout(() => {
setTimer3(false);
}, 3000);
};
return (
<>
<div className="App" style={{ padding: '1rem' }}>
<Button
hasIconOnly
renderIcon={Add16}
tooltipAlignment="center"
tooltipPosition="bottom"
iconDescription="Button description here"
onClick={simulateLoading}
disabled={timer}
/>
</div>
<div className="App" style={{ padding: '1rem' }}>
<Button
hasIconOnly
renderIcon={Add16}
tooltipAlignment="center"
tooltipPosition="bottom"
iconDescription="Button description here"
onClick={simulateLoading2}
disabled={timer2}
/>
</div>
<div className="App" style={{ padding: '1rem' }}>
<Button
hasIconOnly
renderIcon={Add16}
tooltipAlignment="center"
tooltipPosition="bottom"
iconDescription="Button description here"
onClick={simulateLoading3}
disabled={timer3}
/>
</div>
</>
);
};

export const Secondary = () => {
return <Button kind="secondary">Button</Button>;
};
Expand Down Expand Up @@ -226,6 +289,45 @@ export const Playground = () => {
);
};

export const Playground2 = () => {
const regularProps = props.regular();
// const iconOnly = props.iconOnly();
// const { stacked, ...buttonProps } = props.set();
return (
<>
<div
style={{
display: 'flex',
alignItems: 'center',
flexWrap: 'wrap',
}}>
<Button>Buttons</Button>
&nbsp;
{!regularProps.kind.includes('danger') && (
<>
<Button hasIconOnly></Button>
&nbsp;
<Button hasIconOnly kind="ghost"></Button>
</>
)}
</div>
{/* <div
style={{
marginTop: '1rem',
}}>
<ButtonSet stacked={stacked}>
<Button kind="secondary" {...buttonProps}>
Secondary button
</Button>
<Button kind="primary" {...buttonProps}>
Primary button
</Button>
</ButtonSet>
</div> */}
</>
);
};

export const IconButton = () => (
<Button
renderIcon={Add16}
Expand Down
111 changes: 66 additions & 45 deletions packages/react/src/components/Button/Button.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import deprecate from '../../prop-types/deprecate';
import { composeEventHandlers } from '../../tools/events';
import { keys, matches } from '../../internal/keyboard';
import { useId } from '../../internal/useId';
import toggleClass from '../../tools/toggleClass';
import { useFeatureFlag } from '../FeatureFlags';

const { prefix } = settings;
Expand Down Expand Up @@ -47,63 +46,38 @@ const Button = React.forwardRef(function Button(
},
ref
) {
const [allowTooltipVisibility, setAllowTooltipVisibility] = useState(false);
const [isHovered, setIsHovered] = useState(false);
const [isFocused, setIsFocused] = useState(false);
const tooltipRef = useRef(null);
const tooltipTimeout = useRef(null);
const [isTooltipVisible, setTooltipVisible] = useState(false);
const enabled = useFeatureFlag('enable-v11-release');

const closeTooltips = (evt) => {
const tooltipNode = document?.querySelectorAll(`.${prefix}--tooltip--a11y`);
[...tooltipNode].map((node) => {
toggleClass(
node,
`${prefix}--tooltip--hidden`,
node !== evt.currentTarget
);
});
};

const handleFocus = (evt) => {
const handleFocus = () => {
if (hasIconOnly) {
closeTooltips(evt);
setIsHovered(!isHovered);
setIsFocused(true);
setAllowTooltipVisibility(true);
setTooltipVisible(true);
}
};

const handleBlur = () => {
if (hasIconOnly) {
setIsHovered(false);
setIsFocused(false);
setAllowTooltipVisibility(false);
setTooltipVisible(false);
}
};

const handleMouseEnter = (evt) => {
const handleMouseEnter = () => {
if (hasIconOnly) {
setIsHovered(true);
tooltipTimeout.current && clearTimeout(tooltipTimeout.current);

if (evt.target === tooltipRef.current) {
setAllowTooltipVisibility(true);
return;
if (tooltipTimeout.current) {
clearTimeout(tooltipTimeout.current);
}

closeTooltips(evt);

setAllowTooltipVisibility(true);
closeTooltips();
setTooltipVisible(true);
}
};

const handleMouseLeave = () => {
if (!isFocused && hasIconOnly) {
tooltipTimeout.current = setTimeout(() => {
setAllowTooltipVisibility(false);
setIsHovered(false);
}, 100);
}
tooltipTimeout.current = setTimeout(() => {
setTooltipVisible(false);
}, 100);
};

const handleClick = (evt) => {
Expand All @@ -117,15 +91,29 @@ const Button = React.forwardRef(function Button(
useEffect(() => {
const handleEscKeyDown = (event) => {
if (matches(event, [keys.Escape])) {
setAllowTooltipVisibility(false);
setIsHovered(false);
event.stopPropagation();
setTooltipVisible(false);
}
};
document.addEventListener('keydown', handleEscKeyDown);
return () => document.removeEventListener('keydown', handleEscKeyDown);
return () => {
document.removeEventListener('keydown', handleEscKeyDown);
};
}, []);

const enabled = useFeatureFlag('enable-v11-release');
useEffect(() => {
return () => {
if (tooltipTimeout.current) {
clearTimeout(tooltipTimeout.current);
}
};
}, []);

useEffect(() => {
return subscribe((tooltipState) => {
setTooltipVisible(tooltipState);
});
}, []);

const buttonClasses = classNames(className, {
[`${prefix}--btn`]: true,
Expand All @@ -142,8 +130,8 @@ const Button = React.forwardRef(function Button(
[`${prefix}--btn--${kind}`]: kind,
[`${prefix}--btn--disabled`]: disabled,
[`${prefix}--btn--expressive`]: isExpressive,
[`${prefix}--tooltip--hidden`]: hasIconOnly && !allowTooltipVisibility,
[`${prefix}--tooltip--visible`]: isHovered,
[`${prefix}--tooltip--hidden`]: hasIconOnly && !isTooltipVisible,
[`${prefix}--tooltip--visible`]: hasIconOnly && isTooltipVisible,
[`${prefix}--btn--icon-only`]: hasIconOnly,
[`${prefix}--btn--selected`]: hasIconOnly && isSelected && kind === 'ghost',
[`${prefix}--tooltip__trigger`]: hasIconOnly,
Expand Down Expand Up @@ -214,6 +202,7 @@ const Button = React.forwardRef(function Button(
component = 'a';
otherProps = anchorProps;
}

return React.createElement(
component,
{
Expand Down Expand Up @@ -403,4 +392,36 @@ Button.defaultProps = {
isExpressive: false,
};

// The global store for tooltips that are associated with a button. You can cal
// the `closeTooltips` helper to dismiss all currently active tooltips. This
// ensures that only one button tooltip is visible at a time.
//
// Note: closeTooltips should be called _before_ you set the state for a tooltip
// to be visible. Otherwise, the tooltip will become visible and then hidden
// right away.
const listeners = [];

/**
* Subscribe a given callback to be called whenever the state for tooltip
* visibility changes
* @param {Function} callback
* @returns {Function}
*/
function subscribe(callback) {
listeners.push(callback);
return () => {
listeners.splice(listeners.indexOf(callback), 1);
};
}

/**
* This action will close all visible tooltips that are subscribed to the global
* tooltip store
*/
function closeTooltips() {
listeners.forEach((callback) => {
callback(false);
});
}

export default Button;