@carbon/react
Version:
React components for the Carbon Design System
271 lines (263 loc) • 7.94 kB
JavaScript
/**
* Copyright IBM Corp. 2016, 2023
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/
import { extends as _extends } from '../../_virtual/_rollupPluginBabelHelpers.js';
import cx from 'classnames';
import PropTypes from 'prop-types';
import React, { useContext, useRef, useState } from 'react';
import { Popover, PopoverContent } from '../Popover/index.js';
import { Escape } from '../../internal/keyboard/keys.js';
import { match } from '../../internal/keyboard/match.js';
import { useWindowEvent } from '../../internal/useEvent.js';
import { useId } from '../../internal/useId.js';
import { usePrefix } from '../../internal/usePrefix.js';
/**
* Used to render the label for a Toggletip
*/
function ToggletipLabel({
as: BaseComponent = 'span',
children,
className: customClassName,
...rest
}) {
const prefix = usePrefix();
const className = cx(`${prefix}--toggletip-label`, customClassName);
const BaseComponentAsAny = BaseComponent;
return /*#__PURE__*/React.createElement(BaseComponentAsAny, _extends({
className: className
}, rest), children);
}
ToggletipLabel.propTypes = {
/**
* Provide a custom element or component to render the top-level node for the
* component.
*/
as: PropTypes.elementType,
/**
* Custom children to be rendered as the content of the label
*/
children: PropTypes.node,
/**
* Provide a custom class name to be added to the outermost node in the
* component
*/
className: PropTypes.string
};
// Used to coordinate accessibility props between button and content along with
// the actions to open and close the toggletip
const ToggletipContext = /*#__PURE__*/React.createContext(undefined);
function useToggletip() {
return useContext(ToggletipContext);
}
/**
* Used as a container for the button and content of a toggletip. This component
* is responsible for coordinating between interactions with the button and the
* visibility of the content
*/
function Toggletip({
align,
as,
autoAlign,
className: customClassName,
children,
defaultOpen = false,
...rest
}) {
const ref = useRef(null);
const [open, setOpen] = useState(defaultOpen);
const prefix = usePrefix();
const id = useId();
const className = cx(`${prefix}--toggletip`, customClassName, {
[`${prefix}--toggletip--open`]: open,
[`${prefix}--autoalign`]: autoAlign
});
const actions = {
toggle: () => {
setOpen(!open);
},
close: () => {
setOpen(false);
}
};
const value = {
buttonProps: {
'aria-expanded': open,
'aria-controls': id,
'aria-describedby': open ? id : undefined,
onClick: actions.toggle
},
contentProps: {
id
},
onClick: {
onClick: actions.toggle
}
};
const onKeyDown = event => {
if (open && match(event, Escape)) {
event.stopPropagation();
actions.close();
// If the menu is closed while focus is still inside the menu, it should return to the trigger button (#12922)
const button = ref.current?.children[0];
if (button instanceof HTMLButtonElement) {
button.focus();
}
}
};
const handleBlur = event => {
// Do not close if the menu itself is clicked, should only close on focus out
if (open && event.relatedTarget === null) {
return;
}
if (!event.currentTarget.contains(event.relatedTarget)) {
// The menu should be closed when focus leaves the `Toggletip` (#12922)
actions.close();
}
};
// If the `Toggletip` is the last focusable item in the tab order, it should also close when the browser window loses focus (#12922)
useWindowEvent('blur', () => {
if (open) {
actions.close();
}
});
useWindowEvent('click', ({
target
}) => {
if (open && target instanceof Node && !ref.current?.contains(target)) {
actions.close();
}
});
return /*#__PURE__*/React.createElement(ToggletipContext.Provider, {
value: value
}, /*#__PURE__*/React.createElement(Popover, _extends({
align: align,
as: as,
caret: true,
className: className,
dropShadow: false,
highContrast: true,
open: open,
onKeyDown: onKeyDown,
onBlur: handleBlur,
ref: ref,
autoAlign: autoAlign
}, rest), children));
}
// Get all the properties from Popover except for "open".
// The Typescript types for PropTypes are really messed up so we need lots of
// casting. It will be great when we can finally get rid of PropTypes altogether.
const {
open,
...popoverNonOpenPropTypes
} = Popover.propTypes ?? {};
Toggletip.propTypes = {
// Has all of Popover's PropTypes except for "open".
...popoverNonOpenPropTypes,
/**
* Specify if the toggletip should be open by default
*/
defaultOpen: PropTypes.bool
};
/**
* `ToggletipButton` controls the visibility of the Toggletip through mouse
* clicks and keyboard interactions.
*/
const ToggletipButton = /*#__PURE__*/React.forwardRef(function ToggletipButton({
children,
className: customClassName,
label = 'Show information',
as: BaseComponent,
...rest
}, ref) {
const toggletip = useToggletip();
const prefix = usePrefix();
const className = cx(`${prefix}--toggletip-button`, customClassName);
const ComponentToggle = BaseComponent ?? 'button';
if (ComponentToggle !== 'button') {
return /*#__PURE__*/React.createElement(ComponentToggle, _extends({}, toggletip?.onClick, {
className: className
}, rest), children);
}
return /*#__PURE__*/React.createElement("button", _extends({}, toggletip?.buttonProps, {
"aria-label": label,
type: "button",
className: className,
ref: ref
}, rest), children);
});
ToggletipButton.propTypes = {
/**
* Custom children to be rendered as the content of the label
*/
children: PropTypes.node,
/**
* Provide a custom class name to be added to the outermost node in the
* component
*/
className: PropTypes.string,
/**
* Provide an accessible label for this button
*/
label: PropTypes.string
};
ToggletipButton.displayName = 'ToggletipButton';
/**
* `ToggletipContent` is a wrapper around `PopoverContent`. It places the
* `children` passed in as a prop inside of `PopoverContent` so that they will
* be rendered inside of the popover for this component.
*/
const ToggletipContent = /*#__PURE__*/React.forwardRef(function ToggletipContent({
children,
className: customClassName
}, ref) {
const toggletip = useToggletip();
const prefix = usePrefix();
return /*#__PURE__*/React.createElement(PopoverContent, _extends({
className: customClassName
}, toggletip?.contentProps, {
ref: ref
}), /*#__PURE__*/React.createElement("div", {
className: `${prefix}--toggletip-content`
}, children));
});
ToggletipContent.propTypes = {
/**
* Custom children to be rendered as the content of the label
*/
children: PropTypes.node,
/**
* Provide a custom class name to be added to the outermost node in the
* component
*/
className: PropTypes.string
};
ToggletipContent.displayName = 'ToggletipContent';
/**
* `ToggletipActions` is a container for one or two actions present at the base
* of a toggletip. It is used for layout of these items.
*/
function ToggletipActions({
children,
className: customClassName
}) {
const prefix = usePrefix();
const className = cx(`${prefix}--toggletip-actions`, customClassName);
return /*#__PURE__*/React.createElement("div", {
className: className
}, children);
}
ToggletipActions.propTypes = {
/**
* Custom children to be rendered as the content of the label
*/
children: PropTypes.node,
/**
* Provide a custom class name to be added to the outermost node in the
* component
*/
className: PropTypes.string
};
export { Toggletip, ToggletipActions, ToggletipButton, ToggletipContent, ToggletipLabel };