UNPKG

@kiwicom/orbit-components

Version:

Orbit-components is a React component library which provides developers with the easiest possible way of building Kiwi.com's products.

275 lines (273 loc) 12.4 kB
"use strict"; "use client"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default; var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard").default; exports.__esModule = true; exports.default = void 0; var React = _interopRequireWildcard(require("react")); var _clsx = _interopRequireDefault(require("clsx")); var _Heading = _interopRequireDefault(require("../Heading")); var _Stack = _interopRequireDefault(require("../Stack")); var _ButtonLink = _interopRequireDefault(require("../ButtonLink")); var _ChevronDown = _interopRequireDefault(require("../icons/ChevronDown")); var _Slide = _interopRequireDefault(require("../utils/Slide")); var _useRandomId = require("../hooks/useRandomId"); var _useBoundingRect = _interopRequireDefault(require("../hooks/useBoundingRect")); const AnimatedIcon = ({ expanded }) => { return /*#__PURE__*/React.createElement(_ChevronDown.default, { className: (0, _clsx.default)("duration-fast transition-transform ease-in-out", expanded && "rotate-180 "), color: "secondary", ariaHidden: true }); }; /** * @orbit-doc-start * README * ---------- * # Collapse * * To implement Collapse component into your project you'll need to add the import: * * ```jsx * import Collapse from "@kiwicom/orbit-components/lib/Collapse"; * ``` * * After adding import into your project you can use it simply like: * * ```jsx * <Collapse label="Duration"> * <Slider defaultValue={5} onChange={value => doSomething(value)} /> * </Collapse> * ``` * * ## Props * * Table below contains all types of the props available in the Collapse component. * * | Name | Type | Default | Description | * | :------------------ | :---------------------------------- | :------ | :---------------------------------------------------------------------------------------------------------------------- | * | actions | `React.Node` | | Actions which will be render next to arrow. | * | **children** | `React.Node` | | The children that should be collapsed. | * | id | `string` | | Set `id` for `Collapse`. | * | dataTest | `string` | | Optional prop for testing purposes. | * | expanded | `boolean` | | Passing `true` or `false` makes Collapse a controlled component, requiring you to manage its state via `onClick`. | * | initialExpanded | `boolean` | `false` | If `true` the Collapse component will be expanded on the initial render. To be used when the component is uncontrolled. | * | label | `string` | | The rendered label of the Collapse. See accessibility tab. | * | customLabel | `React.Node` | | Allows for rendering any component as a label. See accessibility tab. | * | onClick | `(event, state) => void \| Promise` | | Callback for handling onClick event. | * | expandButtonLabel | `string` | | Required prop for accessible label of the button when the content is collapsed. See accessibility tab. | * | collapseButtonLabel | `string` | | Required prop for accessible label of the button when the content is expanded. See accessibility tab. | * * * Accessibility * ------------- * ## Accessibility * * The Collapse component has been designed with accessibility in mind, providing collapsible content that is fully keyboard accessible and screen reader compatible. * * ### Accessibility Props * * The following props are available to improve the accessibility of your Collapse component: * * | Name | Type | Description | * | :------------------ | :--------- | :------------------------------------------------------------------------------------------------------------------------------------------ | * | expandButtonLabel | string | Specifies the accessible label of the button when the content is collapsed. This should clearly indicate the action to expand the content. | * | collapseButtonLabel | string | Specifies the accessible label of the button when the content is expanded. This should clearly indicate the action to collapse the content. | * | label | string | Defines the visible heading that also serves as the accessible name for the collapsible section. | * | customLabel | React.Node | Allows replacing the default heading with custom content while maintaining the click target for keyboard accessibility. | * * ### Automatic Accessibility Features * * - The component automatically manages ARIA attributes: * - `aria-expanded` is automatically set to `true` or `false` based on the current state. * - `aria-controls` is automatically set to reference the collapsible content. * * ### Best Practices * * - Always provide descriptive `expandButtonLabel` and `collapseButtonLabel` that clearly indicate the action. * - Use clear and concise labels that describe the content being collapsed. * - When using `customLabel`, ensure it includes text that can be announced by screen readers. * - If the collapsed content includes interactive elements, make sure they're not focusable when collapsed. * - All label texts should always be translated to the user's language. * - When using the `actions` prop, ensure all action buttons have descriptive labels and are keyboard accessible to maintain a logical tab order in the component. * * ### Focus Management * * - When expanded, focus moves from the label to the button to the content inside. * - When collapsed, focus will skip over any focusable elements inside the collapsed content. * * ### Keyboard Navigation * * - **Tab**: Moves focus to the collapse label or expand/collapse button. * - **Enter** or **Space**: When focus is on the label or button, toggles the collapsed state. * - Focus order: Focus moves from the label to the button to the content inside (when expanded). * - When collapsed, focus will skip over any focusable elements inside the collapsed content. * * ### Examples * * #### Basic Usage with Required Accessibility Labels * * ```jsx * import { Collapse, Text } from "@kiwicom/orbit-components"; * * <Collapse * label="Flight Information" * expandButtonLabel="Show flight details" * collapseButtonLabel="Hide flight details" * > * <Text>Flight number: KL1234</Text> * <Text>Departure: 10:00</Text> * <Text>Arrival: 12:30</Text> * </Collapse>; * ``` * * Screen reader announces: "Flight Information, collapsed, button" when focused on the label and "Show flight details, collapsed, button" when focused on the button. * * #### Expanded State * * ```jsx * import { Collapse, Text } from "@kiwicom/orbit-components"; * * <Collapse * label="Passenger Details" * expandButtonLabel="Show passenger details" * collapseButtonLabel="Hide passenger details" * expanded={true} * > * <Text>Name: John Smith</Text> * <Text>Passport: AB123456</Text> * </Collapse>; * ``` * * Screen reader announces: "Passenger Details, expanded, button" when focused on the label and "Hide passenger details, expanded, button" when focused on the button. * * #### With Actions * * ```jsx * import { Collapse, Button, Text } from "@kiwicom/orbit-components"; * * <Collapse * label="Filter Options" * expandButtonLabel="Show filters" * collapseButtonLabel="Hide filters" * actions={ * <Button type="white" size="small"> * Reset * </Button> * } * > * <Text>Filter controls...</Text> * </Collapse>; * ``` * * Screen reader announces: "Filter Options, collapsed, button" when focused on the label, "Reset, button" when focused on the reset button, and "Show filters, collapsed, button" when focused on the button. * * #### With Custom Label * * ```jsx * import { Collapse, Badge, Text } from "@kiwicom/orbit-components"; * * <Collapse * customLabel={<Badge type="info">2 baggages</Badge>} * expandButtonLabel="Show baggage details" * collapseButtonLabel="Hide baggage details" * > * <Text>Cabin baggage: 1 × 8kg</Text> * <Text>Checked baggage: 1 × 23kg</Text> * </Collapse>; * ``` * * Screen reader announces: "Two baggages collapsed, button" when focused on the custom label and "Show baggage details, collapsed, button" when focused on the button. * * * @orbit-doc-end */ const Collapse = ({ initialExpanded = false, customLabel, expanded: expandedProp, label, children, dataTest, id, onClick, actions, expandButtonLabel, collapseButtonLabel }) => { const isControlledComponent = expandedProp != null; const [expandedState, setExpandedState] = React.useState(isControlledComponent ? expandedProp : initialExpanded); const expanded = isControlledComponent ? expandedProp : expandedState; const [{ height }, node] = (0, _useBoundingRect.default)({ height: expanded ? null : 0 }); const randomId = (0, _useRandomId.useRandomIdSeed)(); const slideID = randomId("slideID"); const labelID = randomId("labelID"); const handleClick = React.useCallback(event => { if (!isControlledComponent) { if (onClick) { onClick(event, !expanded); } setExpandedState(!expanded); } else if (onClick) { onClick(event, !expanded); } }, [expanded, isControlledComponent, onClick]); return /*#__PURE__*/React.createElement("div", { className: "border-b-cloud-normal pb-300 mb-400 block w-full border-b border-solid last:m-0 last:border-none", "data-test": dataTest, id: id }, /*#__PURE__*/React.createElement("div", { className: "flex items-center justify-between" }, label || customLabel ? /*#__PURE__*/React.createElement("div", { className: "flex w-full self-stretch", id: labelID, role: "button", tabIndex: 0, "aria-expanded": expanded, "aria-controls": slideID, onClick: handleClick, onKeyDown: e => { if (e.key === "Enter" || e.key === " ") { handleClick(e); } } }, /*#__PURE__*/React.createElement(_Stack.default, { justify: "between", align: "center" }, label && !customLabel && /*#__PURE__*/React.createElement(_Heading.default, { type: "title4" }, label), customLabel)) : /*#__PURE__*/React.createElement("div", { className: "flex w-full self-stretch" }), /*#__PURE__*/React.createElement(_Stack.default, { inline: true, grow: false, align: "center", spacing: "none" }, actions && /*#__PURE__*/React.createElement("div", { className: "mx-300 flex items-center" }, actions), /*#__PURE__*/React.createElement(_ButtonLink.default, { iconLeft: /*#__PURE__*/React.createElement(AnimatedIcon, { expanded: expanded }), size: "small", type: "secondary", title: expanded ? collapseButtonLabel : expandButtonLabel, onClick: handleClick, ariaControls: slideID, ariaExpanded: expanded }))), /*#__PURE__*/React.createElement(_Slide.default, { maxHeight: height, expanded: expanded, id: slideID }, /*#__PURE__*/React.createElement("div", { className: "my-300 mx-0", ref: node }, children))); }; var _default = exports.default = Collapse;