@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.
293 lines (292 loc) • 13.4 kB
JavaScript
"use strict";
"use client";
var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard").default;
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
exports.__esModule = true;
exports.default = void 0;
var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends"));
var React = _interopRequireWildcard(require("react"));
var _clsx = _interopRequireDefault(require("clsx"));
var _SeatLegend = _interopRequireDefault(require("./components/SeatLegend"));
exports.SeatLegend = _SeatLegend.default;
var _Stack = _interopRequireDefault(require("../Stack"));
var _Text = _interopRequireDefault(require("../Text"));
var _useRandomId = require("../hooks/useRandomId");
var _SeatNormal = _interopRequireDefault(require("./components/SeatNormal"));
var _SeatSmall = _interopRequireDefault(require("./components/SeatSmall"));
var _SeatCircle = _interopRequireDefault(require("./components/SeatCircle"));
var _consts = require("./consts");
/**
* @orbit-doc-start
* README
* ----------
* # Seat
*
* To implement Seat component into your project you'll need to add the import:
*
* ```jsx
* import Seat, { SeatLegend } from "@kiwicom/orbit-components/lib/Seat";
* ```
*
* After adding import into your project you can use it simply like:
*
* ```jsx
* <Seat />
* ```
*
* ## Props
*
* Table below contains all types of the props available in Seat component.
*
* | Name | Type | Default | Description |
* | :-------------- | :---------------------- | :-------- | :-------------------------------------------------------------------------------------- |
* | dataTest | `string` | | Optional prop for testing purposes. |
* | id | `string` | | `id` of the element. |
* | size | [`enum`](#modal-enum) | `medium` | Size of Seat component. |
* | type | [`enum`](#modal-enum) | `default` | Visual type of Seat. If `unavailable`, the element becomes disabled. |
* | price | `string` | | Price of Seat. Displayed as text underneath the svg. |
* | label | `string` | | Label text inside of a Seat. Not announced by screen readers. |
* | selected | `boolean` | | Displays Seat as selected. |
* | onClick | `() => void \| Promise` | | Function for handling onClick event. |
* | aria-labelledby | `string` | | Id(s) of elements that announce the component to screen readers. See accessibility tab. |
* | title | `string` | | Adds title title to svg element. Announced by screen readers. See accessibility tab. |
* | description | `string` | | Adds description to svg element. Announced by screen readers. See accessibility tab. |
*
* ## SeatLegend
*
* Table below contains all types of the props available in Seat/SeatLegend component.
*
* | Name | Type | Default | Description |
* | :--------- | :-------------------- | :-------- | :----------------------------------------------------------------------------------------------------------- |
* | dataTest | `string` | | Optional prop for testing purposes. |
* | id | `string` | | `id` of the element. |
* | type | [`enum`](#modal-enum) | `default` | Visual type of the rendered seat icon. |
* | label | `string` | | Label text to be displayed next to the seat icon. |
* | aria-label | `string` | | Adds `aria-label` attribute to the rendered SVG element. Announced by screen readers. See accessibility tab. |
*
* ### enum
*
* | size | type |
* | :--------- | :-------------- |
* | `"small"` | `"default"` |
* | `"medium"` | `"legroom"` |
* | | `"unavailable"` |
*
*
* Accessibility
* -------------
* # Accessibility
*
* ## Seat
*
* The Seat component has been designed with accessibility in mind.
*
* It can be used with keyboard navigation, and it includes the following properties that allow to improve the experience for users of assistive technologies:
*
* | Name | Type | Description |
* | :-------------- | :------- | :-------------------------------------------------------------------------------------------------------------------------- |
* | aria-labelledby | `string` | Id(s) of elements that announce the component to screen readers. |
* | title | `string` | Adds the `title` attribute to the rendered SVG element. Announced by screen readers after the `aria-labelledby` element(s). |
* | description | `string` | Adds the `description` attribute to the rendered SVG element. Announced by screen readers after the `title` value. |
*
* All the props above are optional, but recommended to use to ensure the best experience for all users.
*
* The `aria-labelledby` prop can reference multiple ids, separated by a space.
* The elements with those ids can be hidden, so that their text is only announced by screen readers.
*
* The `title` and `description` props are used to provide additional context to the rendered SVG element that visually represents the seat.
* They are also announced by screen readers.
*
* The conjugation of these properties allows to provide a detailed description of the seat to users of assistive technologies.
*
* ## SeatLegend
*
* The SeatLegend component is not interactive. However, it accepts the `aria-label` prop, that is passed to the rendered SVG element.
*
* It allows for screen readers to provide a meaningful description of the seat type, which can be useful for users of assistive technologies.
*
* The `label` prop is also announced by screen readers.
*
* ### Automatic Accessibility Features
*
* - The component automatically manages ARIA attributes:
* - Uses `aria-pressed` to communicate the selection state of the seat
* - Sets `aria-disabled="true"` for unavailable seats
* - Combines `aria-labelledby`, `title`, and `description` for comprehensive screen reader announcements
* - Focus management is handled automatically:
* - Seats with `onClick` function are rendered as interactive buttons with proper focus indicators
* - State management is handled automatically:
* - Selected state is visually indicated with a highlight
* - Unavailable seats have distinct styling to indicate they cannot be selected
*
* ### Best Practices
*
* - Always provide either `title`, `aria-labelledby`, or both to ensure seats have accessible names.
* - Include meaningful `description` for seats with special characteristics (like extra legroom).
* - Consider adding an explanation of seat types using the SeatLegend component.
*
* ### Keyboard Navigation
*
* - **Tab**: Moves focus between interactive (available) seats
* - **Space/Enter**: Selects or deselects the focused seat
* - Focus order should follow a logical pattern, typically left-to-right and top-to-bottom
*
* ### Examples
*
* #### Basic Seat with Accessibility Labels
*
* ```jsx
* <p id="l1" style={{ display: "none", visibility: "hidden" }}>
* For passenger John Doe
* </p>
* <Seat
* aria-labelledby="l1"
* title="Seat 1A"
* description="Extra legroom"
* label="25€"
* />
* ```
*
* It would have the screen reader announce: "For passenger John Doe. Seat 1A. Extra legroom.".
*
* Note that the `label` prop is **not** announced by screen readers, as it is intended for visual representation only.
* So be sure to include all relevant information on the three properties that are announced by screen readers.
*
* Alternatively, the paragraph element with the id `l1` is visually hidden, so that its text is only read by screen readers but not present on the screen.
*
* It is also recommended to have those strings translated and change dynamically based on the state of the user journey (eg: if the seat is selected and the user is about to deselect it, the screen reader should announce it).
*
* #### Selected Seat with Price
*
* ```jsx
* <Seat
* title="Extra legroom seat 1C"
* description="Aisle seat with extra legroom"
* type="legroom"
* selected
* price="€15"
* onClick={() => handleSeatSelection("1C")}
* />
* ```
*
* Screen reader announces: "Extra legroom seat 1C, Aisle seat with extra legroom, button, pressed".
*
* #### Unavailable Seat
*
* ```jsx
* <Seat title="Seat 24B already occupied" type="unavailable" />
* ```
*
* Screen reader announces: "Seat 24B already occupied, dimmed, toggle button".
*
* #### Using External Labels
*
* ```jsx
* <div>
* <span id="seat-label" className="sr-only">
* Exit row seat 15F
* </span>
* <span id="seat-desc" className="sr-only">
* Extra legroom, additional responsibilities
* </span>
* <Seat
* type="legroom"
* aria-labelledby="seat-label seat-desc"
* onClick={() => handleSeatSelection("15F")}
* />
* </div>
* ```
*
* Mentioned span elements are visually hidden, so that their text is only read by screen readers.
*
* Screen reader announces: "Exit row seat 15F Extra legroom, additional responsibilities, toggle button".
*
* #### Seat Legend with Accessibility Label
*
* ```jsx
* <div>
* <Seat
* type="legroom"
* title="15F"
* description="Window seat"
* aria-labelledby="legend"
* onClick={() => handleSeatSelection("15F")}
* />{" "}
* <SeatLegend
* id="legend"
* type="legroom"
* label="Extra legroom"
* aria-label="This seat has extra legroom"
* />{" "}
* </div>
* ```
*
* Screen reader announces: "This seat has extra legroom 15F Window seat, toggle button" once the Seat component is focused, and "This seat has extra legroom" once the SeatLegend's legend icon is focused.
*
*
* @orbit-doc-end
*/
const Seat = ({
type = _consts.TYPES.DEFAULT,
selected = false,
onClick,
size = _consts.SIZE_OPTIONS.MEDIUM,
dataTest,
id,
price,
label,
title,
description,
"aria-labelledby": ariaLabelledBy = ""
}) => {
const randomId = (0, _useRandomId.useRandomIdSeed)();
const titleId = title ? randomId("title") : "";
const descrId = description ? randomId("descr") : "";
const isAvailable = type !== _consts.TYPES.UNAVAILABLE;
const clickable = isAvailable && onClick !== undefined;
const commonProps = {
className: (0, _clsx.default)("orbit-seat font-base group relative", isAvailable && "cursor-pointer", size === _consts.SIZE_OPTIONS.SMALL ? "w-800 h-[36px]" : "size-[46px]"),
id,
"data-test": dataTest
};
const seatContent = /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("svg", {
viewBox: size === _consts.SIZE_OPTIONS.SMALL ? "0 0 32 36" : "0 0 46 46",
"aria-labelledby": `${[ariaLabelledBy, titleId, descrId].join(" ").trim()}` || undefined,
fill: "none",
role: "img"
}, title && /*#__PURE__*/React.createElement("title", {
id: titleId
}, title), description && /*#__PURE__*/React.createElement("desc", {
id: descrId
}, description), size === _consts.SIZE_OPTIONS.SMALL ? /*#__PURE__*/React.createElement(_SeatSmall.default, {
type: type,
selected: selected,
label: label
}) : /*#__PURE__*/React.createElement(_SeatNormal.default, {
type: type,
selected: selected,
label: label
})), selected && isAvailable && /*#__PURE__*/React.createElement(_SeatCircle.default, {
size: size,
type: type
}));
return /*#__PURE__*/React.createElement(_Stack.default, {
inline: true,
grow: false,
spacing: "50",
direction: "column",
align: "center"
}, clickable ? /*#__PURE__*/React.createElement("button", (0, _extends2.default)({}, commonProps, {
onClick: onClick,
type: "button",
"aria-pressed": selected
}), seatContent) : /*#__PURE__*/React.createElement("div", (0, _extends2.default)({}, commonProps, {
role: "button",
"aria-pressed": selected,
"aria-disabled": "true"
}), seatContent), price && !(selected && !isAvailable) && /*#__PURE__*/React.createElement(_Text.default, {
size: "small",
type: "secondary"
}, price));
};
var _default = exports.default = Seat;