@carbon/ibm-products
Version:
Carbon for IBM Products
249 lines (247 loc) • 8.99 kB
JavaScript
/**
* Copyright IBM Corp. 2020, 2026
*
* 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 { __toESM } from "../../_virtual/_rolldown/runtime.js";
import { require_classnames } from "../../node_modules/classnames/index.js";
import { pkg } from "../../settings.js";
import { useControllableState } from "../../global/js/hooks/useControllableState.js";
import { usePrefersReducedMotion } from "../../global/js/hooks/usePrefersReducedMotion.js";
import { getDevtoolsProps } from "../../global/js/utils/devtools.js";
import uuidv4 from "../../global/js/utils/uuidv4.js";
import React, { useRef, useState } from "react";
import PropTypes from "prop-types";
import { Heading, Layer, Section, Toggle } from "@carbon/react";
import { ChevronDown, Locked, WarningAltFilled, WarningFilled } from "@carbon/react/icons";
import * as carbonMotion from "@carbon/motion";
import { useNoInteractiveChildren } from "@carbon/utilities-react";
//#region src/components/OptionsTile/OptionsTile.tsx
/**
* Copyright IBM Corp. 2021, 2024
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/
var import_classnames = /* @__PURE__ */ __toESM(require_classnames());
const blockClass = `${pkg.prefix}--options-tile`;
const componentName = "OptionsTile";
const defaults = { size: "lg" };
const OptionsTile = React.forwardRef((props, ref) => {
const { children, className, enabled, invalid, invalidText, locked, lockedText, onChange, onToggle, open: userOpen, size = defaults.size, summary, title, titleId: userDefinedTitleId, warn, warnText, ...rest } = props;
const [closing, setClosing] = useState(false);
const [open, setOpen] = useControllableState(userOpen ?? false, onChange);
const detailsRef = useRef(null);
const contentRef = useRef(null);
const headingRef = useRef(null);
useNoInteractiveChildren(headingRef);
const titleId = userDefinedTitleId ?? `${uuidv4()}-title`;
const isExpandable = children !== void 0;
const isLocked = !invalid && !(!invalid && warn) && locked;
const shouldReduceMotion = usePrefersReducedMotion();
const expand = () => {
if (detailsRef.current && contentRef.current && !shouldReduceMotion) {
setOpen(true);
detailsRef.current.open = true;
const { paddingTop, paddingBottom, height } = getComputedStyle(contentRef.current);
contentRef.current.animate([{
paddingTop: 0,
paddingBottom: 0,
height: 0,
opacity: 0,
overflow: "hidden"
}, {
paddingTop,
paddingBottom,
height,
opacity: 1,
overflow: "hidden"
}], {
duration: Number(carbonMotion.moderate01.replace("ms", "")),
easing: carbonMotion.easings.entrance.productive
});
} else setOpen(true);
};
const collapse = () => {
if (contentRef.current && !shouldReduceMotion) {
setClosing(true);
const { paddingTop, paddingBottom, height } = getComputedStyle(contentRef.current);
const animationDuration = Number(carbonMotion.moderate01.replace("ms", ""));
const animation = contentRef.current.animate([{
paddingTop,
paddingBottom,
height,
opacity: 1
}, {
paddingTop: 0,
paddingBottom: 0,
height: 0,
opacity: 0
}], {
duration: animationDuration,
easing: carbonMotion.easings.entrance.productive
});
const callback = () => {
setOpen(false);
setClosing(false);
};
setTimeout(() => {
callback();
}, animationDuration * .9);
animation.oncancel = callback;
} else setOpen(false);
};
const handleSummaryClick = (evt) => {
if (evt.target.closest(`.${blockClass}__toggle-container`)) {
evt.preventDefault();
evt.stopPropagation();
return;
}
evt.preventDefault();
if (open) collapse();
else expand();
};
const renderTitle = () => {
let Icon = null;
let text = summary;
if (invalid) {
Icon = WarningFilled;
text = invalidText;
} else if (warn) {
Icon = WarningAltFilled;
text = warnText;
} else if (locked) {
Icon = Locked;
if (!text) text = lockedText;
}
const summaryHidden = !(invalid || warn || locked) && enabled === false;
const summaryClasses = (0, import_classnames.default)(`${blockClass}__summary`, {
[`${blockClass}__summary--closing`]: closing,
[`${blockClass}__summary--open`]: open,
[`${blockClass}__summary--invalid`]: invalid,
[`${blockClass}__summary--warn`]: warn,
[`${blockClass}__summary--locked`]: locked,
[`${blockClass}__summary--hidden`]: summaryHidden
});
return /* @__PURE__ */ React.createElement("div", { className: `${blockClass}__heading` }, /* @__PURE__ */ React.createElement(Heading, {
ref: headingRef,
id: titleId,
className: `${blockClass}__title`
}, title), text && /* @__PURE__ */ React.createElement("span", {
className: summaryClasses,
"aria-hidden": summaryHidden
}, Icon && /* @__PURE__ */ React.createElement(Icon, { size: 16 }), /* @__PURE__ */ React.createElement("span", { className: `${blockClass}__summary-text` }, text)));
};
return /* @__PURE__ */ React.createElement(Section, {
...rest,
className: (0, import_classnames.default)(blockClass, className, `${blockClass}--${size}`, { [`${blockClass}--closing`]: closing }),
ref,
...getDevtoolsProps(componentName)
}, isExpandable ? /* @__PURE__ */ React.createElement("details", {
className: `${blockClass}__details`,
open,
ref: detailsRef
}, /* @__PURE__ */ React.createElement("summary", {
className: (0, import_classnames.default)(`${blockClass}__header`, { [`${blockClass}__header--has-toggle`]: enabled !== void 0 }),
onClick: handleSummaryClick,
"data-testid": "options-tile-header"
}, enabled !== void 0 && /* @__PURE__ */ React.createElement("div", {
className: `${blockClass}__toggle-container`,
"data-testid": "options-tile-toggle-container",
onMouseDown: (evt) => {
evt.preventDefault();
}
}, /* @__PURE__ */ React.createElement(Toggle, {
id: `${titleId}-toggle`,
className: `${blockClass}__toggle`,
toggled: enabled,
"aria-labelledby": titleId,
hideLabel: true,
onToggle,
size: "sm",
disabled: isLocked
})), /* @__PURE__ */ React.createElement(ChevronDown, {
size: 16,
className: (0, import_classnames.default)(`${blockClass}__chevron`, {
[`${blockClass}__chevron--open`]: open,
[`${blockClass}__chevron--closing`]: closing
})
}), renderTitle()), /* @__PURE__ */ React.createElement("div", {
className: `${blockClass}__content`,
ref: contentRef,
"data-testid": "options-tile-content"
}, /* @__PURE__ */ React.createElement(Layer, null, isLocked && /* @__PURE__ */ React.createElement("p", { className: `${blockClass}__locked-text` }, /* @__PURE__ */ React.createElement(Locked, { size: 16 }), lockedText), children))) : /* @__PURE__ */ React.createElement("div", { className: `${blockClass}__static-content` }, renderTitle()));
});
OptionsTile.displayName = componentName;
OptionsTile.propTypes = {
/**
* Provide content to render as expandable OptionsTile. If no children
* are present, the OptionsTile will render as its variant.
*/
children: PropTypes.node,
/**
* Provide an optional class to be applied to the containing node.
*/
className: PropTypes.string,
/**
* Whether the toggle is enabled or disabled. If nothing is passed,
* no toggle will be rendered.
*/
enabled: PropTypes.bool,
/**
* Whether the OptionsTile is in invalid validation state.
*/
invalid: PropTypes.bool,
/**
* Provide a text explaining why the OptionsTile is in invalid state.
*/
invalidText: PropTypes.string,
/**
* Whether the OptionsTile is in locked validation state.
*/
locked: PropTypes.bool,
/**
* Provide a text explaining why the OptionsTile is in locked state.
*/
lockedText: PropTypes.string,
/**
* A handler for managing the controlled state of open prop. If not passed the open prop will not be honored and an uncontrolled state will be used.
*/
onChange: PropTypes.func,
/**
* Provide a function which will be called each time the user
* interacts with the toggle.
*/
onToggle: PropTypes.func,
/**
* For controlled usage of the tile open state. This prop only works when an onChange prop is also passed, otherwise an uncontrolled state is used.
*/
open: PropTypes.bool,
/**
* Define the size of the OptionsTile.
*/
size: PropTypes.oneOf(["lg", "xl"]),
/**
* Optionally provide a text summarizing the current state of the content.
*/
summary: PropTypes.string,
/**
* Provide the title for this OptionsTile. Must not contain any interactive elements.
*/
title: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired,
/**
* Optionally provide an id which should be used for the title.
*/
titleId: PropTypes.string,
/**
* Whether the OptionsTile is in warning validation state.
*/
warn: PropTypes.bool,
/**
* Provide a text explaining why the OptionsTile is in warning state.
*/
warnText: PropTypes.string
};
//#endregion
export { OptionsTile };