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

feat(Seat): add status prop with processing/done states, deprecate selected #4567

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 39 additions & 6 deletions packages/orbit-components/src/Seat/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ Table below contains all types of the props available in 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. |
| selected | `boolean` | | **Deprecated.** Use `status="selected"` instead. |
| status | [`enum`](#status-enum) | `default` | Visual status of the seat (default, selected, processing, done). |
| onClick | `() => void \| Promise` | | Function for handling onClick event. |
| aria-labelledby | `string` | | Id(s) of elements that announce the component to screen readers. |
| title | `string` | | Adds title title to svg element. Announced by screen readers. |
Expand All @@ -44,8 +45,40 @@ Table below contains all types of the props available in Seat/SeatLegend compone

### enum

| size | type |
| :--------- | :-------------- |
| `"small"` | `"default"` |
| `"medium"` | `"legroom"` |
| | `"unavailable"` |
| size | type | status |
| :--------- | :-------------- | :------------- |
| `"small"` | `"default"` | `"default"` |
| `"medium"` | `"legroom"` | `"selected"` |
| | `"unavailable"` | `"processing"` |
| | | `"done"` |

### Status States

The `status` prop determines the visual appearance of the seat:

- `default`: Default state of the seat
- `selected`: Shows the seat as selected (replaces the deprecated `selected` prop)
- `processing`: Shows a processing state with:
- Background color: product light active
- Label: bold text in product darker color
- Icon: Reload icon in the top-right corner
- `done`: Shows a completed state with:
- Background color: product light active
- Label: bold text in product darker color
- Icon: Check icon in the top-right corner

Example usage:

```jsx
// Processing state
<Seat status="processing" label="12A" />

// Done state
<Seat status="done" label="12A" />

// Selected state (new way)
<Seat status="selected" label="12A" />

// Selected state (deprecated way)
<Seat selected label="12A" />
```
41 changes: 39 additions & 2 deletions packages/orbit-components/src/Seat/components/SeatCircle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import cx from "clsx";

import { useRandomIdSeed } from "../../hooks/useRandomId";
import { SIZE_OPTIONS, TYPES } from "../consts";
import type { Type, Size } from "../types";
import type { Type, Size, SeatStatus } from "../types";
import Icon from "../../Icon";
import { ICON_SIZES, ICON_COLORS } from "../../Icon/consts";

const defaultClasses = {
[TYPES.LEGROOM]: "fill-blue-dark-hover",
Expand All @@ -20,15 +22,50 @@ const hoverClasses = {
interface Props {
type: Type;
size: Size;
/**
* @deprecated Use `status="selected"` instead
*/
selected?: boolean;
status?: SeatStatus;
}

const SeatCircle = ({ size, type }: Props) => {
const SeatCircle = ({ size, type, selected, status }: Props) => {
const effectiveStatus = status ?? (selected ? "selected" : "default");
const renderIcon = () => {
switch (effectiveStatus) {
case "processing":
return (
<Icon
size={ICON_SIZES.SMALL}
color={ICON_COLORS.PRIMARY}
viewBox="0 0 24 24"
className="absolute right-[8px] top-[8px]"
>
<path d="M15.3079 4.56755C14.2643 4.09674 13.1286 3.85 11.9598 3.85C7.45865 3.85 3.80977 7.49888 3.80977 12C3.80977 12.4774 3.85092 12.9493 3.93174 13.4117L2.20322 14.2924C1.87781 14.4582 1.77113 14.7851 1.93721 15.1111C2.02011 15.2738 2.12961 15.3546 2.29281 15.4081L6.22308 16.6846C6.49605 16.8181 6.82146 16.6523 6.92814 16.3254L8.20499 12.3957C8.31139 12.0682 8.14559 11.7428 7.81811 11.6364L7.72275 11.6112C7.59886 11.5869 7.48572 11.6008 7.35556 11.6671L5.63277 12.545C5.61748 12.3647 5.60977 12.1829 5.60977 12C5.60977 8.493 8.45276 5.65 11.9598 5.65C12.8721 5.65 13.7556 5.84196 14.5678 6.20833C15.0209 6.41272 15.5539 6.21111 15.7582 5.75802C15.9626 5.30494 15.761 4.77194 15.3079 4.56755ZM21.6938 8.49822L17.7635 7.22165C17.4905 7.08815 17.1651 7.25396 17.0585 7.58089L15.7816 11.5106C15.6752 11.8381 15.841 12.1635 16.1685 12.2699L16.2638 12.2951C16.3877 12.3194 16.5009 12.3055 16.631 12.2392L18.2817 11.3987C18.3004 11.5974 18.3098 11.798 18.3098 12C18.3098 15.507 15.4668 18.35 11.9598 18.35C11.0517 18.35 10.172 18.1598 9.3629 17.7967C8.90942 17.5932 8.37682 17.7958 8.1733 18.2493C7.96979 18.7028 8.17243 19.2354 8.62592 19.4389C9.66571 19.9055 10.7964 20.15 11.9598 20.15C16.4609 20.15 20.1098 16.5011 20.1098 12C20.1098 11.5037 20.0653 11.0132 19.978 10.5332L21.7834 9.61393C22.1088 9.44813 22.2155 9.12119 22.0494 8.79524C21.9665 8.63253 21.857 8.5517 21.6938 8.49822Z" />
</Icon>
);
case "done":
return (
<Icon
size={ICON_SIZES.SMALL}
color={ICON_COLORS.PRIMARY}
viewBox="0 0 24 24"
className="absolute right-[8px] top-[8px]"
>
<path d="M6.44495 12.6675C6.10185 12.3078 5.53216 12.2944 5.17251 12.6375C4.81286 12.9806 4.79945 13.5503 5.14255 13.91L8.71513 17.6548C9.08299 18.0404 9.70349 18.0237 10.05 17.6188L18.6412 7.58219C18.9644 7.20458 18.9203 6.63644 18.5427 6.31321C18.1651 5.98998 17.5969 6.03407 17.2737 6.41168L9.67551 15.2883C9.49068 15.5043 9.15975 15.5132 8.96355 15.3075L6.44495 12.6675Z" />
</Icon>
);
default:
return null;
}
};
const randomId = useRandomIdSeed();
const circleSmallId = randomId("circleSmallId");
const circleNormalId = randomId("circleNormalId");

return (
<div className="absolute right-[-10px] top-[-10px]">
{renderIcon()}
{size === SIZE_OPTIONS.SMALL ? (
<svg width="22" height="22" viewBox="0 0 22 22" fill="none">
<mask
Expand Down
11 changes: 7 additions & 4 deletions packages/orbit-components/src/Seat/components/SeatNormal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,24 @@ import SymbolUnavailable from "./primitives/SymbolUnavailable";
import Text from "./primitives/Text";
import Edge from "./primitives/Edge";

const SeatNormal = ({ selected, type, label }: SeatVariantProps) => {
if (selected && type === TYPES.UNAVAILABLE) return null;
const SeatNormal = ({ selected, status, type, label }: SeatVariantProps) => {
const effectiveStatus = status ?? (selected ? "selected" : "default");
if (effectiveStatus === "selected" && type === TYPES.UNAVAILABLE) return null;

return (
<>
<TransitionPathFill
type={type}
selected={selected}
status={effectiveStatus}
d="M1 10C1 5.02944 5.02944 1 10 1H36C40.9706 1 45 5.02944 45 10V42C45 43.6569 43.6569 45 42 45H4C2.34315 45 1 43.6569 1 42V10Z"
/>
{type === TYPES.UNAVAILABLE ? (
<SymbolUnavailable d="M28.6555 17.3331C29.0072 17.6844 29.0077 18.2542 28.6564 18.6059L24.5533 22.7144C24.3973 22.8705 24.3973 23.1235 24.5533 23.2797L28.6564 27.3885C28.9784 27.7109 29.0049 28.2166 28.7361 28.5692L28.6555 28.6613C28.3038 29.0125 27.7339 29.0121 27.3827 28.6603L23.2821 24.5535C23.1258 24.397 22.8722 24.3971 22.7159 24.5536L18.6169 28.6603C18.2657 29.0121 17.6958 29.0125 17.3441 28.6613C16.9923 28.3101 16.9919 27.7402 17.3431 27.3885L21.4458 23.2796C21.6017 23.1235 21.6017 22.8705 21.4458 22.7144L17.3431 18.6059C17.0212 18.2835 16.9947 17.7778 17.2635 17.4252L17.3441 17.3331C17.6958 16.9819 18.2657 16.9823 18.6169 17.3341L22.716 21.4396C22.8722 21.596 23.1258 21.5961 23.2821 21.4396L27.3827 17.3341C27.7339 16.9823 28.3038 16.9819 28.6555 17.3331Z" />
) : (
<Text selected={selected} type={type} label={label} fontSize={16} />
<Text selected={selected} status={effectiveStatus} type={type} label={label} fontSize={16} />
)}
{!selected && (
{effectiveStatus === "default" && (
<Edge
type={type}
d="M0 40H46V42C46 44.2091 44.2091 46 42 46H4C1.79086 46 0 44.2091 0 42V40Z"
Expand All @@ -32,6 +34,7 @@ const SeatNormal = ({ selected, type, label }: SeatVariantProps) => {
<TransitionPathStroke
type={type}
selected={selected}
status={effectiveStatus}
d="M1 10C1 5.02944 5.02944 1 10 1H36C40.9706 1 45 5.02944 45 10V42C45 43.6569 43.6569 45 42 45H4C2.34315 45 1 43.6569 1 42V10Z"
/>
</>
Expand Down
11 changes: 7 additions & 4 deletions packages/orbit-components/src/Seat/components/SeatSmall.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,30 @@ import SymbolUnavailable from "./primitives/SymbolUnavailable";
import Text from "./primitives/Text";
import Edge from "./primitives/Edge";

const SeatSmall = ({ type, selected, label }: SeatVariantProps) => {
if (selected && type === TYPES.UNAVAILABLE) return null;
const SeatSmall = ({ type, selected, status, label }: SeatVariantProps) => {
const effectiveStatus = status ?? (selected ? "selected" : "default");
if (effectiveStatus === "selected" && type === TYPES.UNAVAILABLE) return null;

return (
<>
<TransitionPathFill
type={type}
selected={selected}
status={effectiveStatus}
d="M1 10C1 5.02944 5.02944 1 10 1H22C26.9706 1 31 5.02944 31 10V32C31 33.6569 29.6569 35 28 35H4C2.34315 35 1 33.6569 1 32V10Z"
/>
{type === TYPES.UNAVAILABLE ? (
<SymbolUnavailable d="M20.7129 13.2776C21.006 13.5703 21.0064 14.0451 20.7137 14.3383L17.3415 17.7148C17.1855 17.871 17.1855 18.124 17.3414 18.2801L20.7137 21.6571C20.982 21.9257 21.0041 22.3472 20.7801 22.641L20.7129 22.7177C20.4198 23.0104 19.9449 23.01 19.6523 22.7169L16.2823 19.3419C16.126 19.1853 15.8724 19.1854 15.7161 19.3419L12.3474 22.7169C12.0547 23.01 11.5798 23.0104 11.2867 22.7177C10.9936 22.425 10.9933 21.9502 11.2859 21.6571L14.6578 18.2801C14.8137 18.124 14.8137 17.871 14.6578 17.7149L11.2859 14.3383C11.0177 14.0696 10.9956 13.6482 11.2195 13.3544L11.2867 13.2776C11.5798 12.9849 12.0547 12.9853 12.3474 13.2784L15.7162 16.6524C15.8724 16.8089 16.126 16.8089 16.2822 16.6524L19.6523 13.2784C19.9449 12.9853 20.4198 12.9849 20.7129 13.2776Z" />
) : (
<Text selected={selected} type={type} label={label} fontSize={14} />
<Text selected={selected} status={effectiveStatus} type={type} label={label} fontSize={14} />
)}
{!selected && (
{effectiveStatus === "default" && (
<Edge type={type} d="M0 32H32C32 34.2091 30.2091 36 28 36H4C1.79086 36 0 34.2091 0 32Z" />
)}
<TransitionPathStroke
type={type}
selected={selected}
status={effectiveStatus}
d="M1 10C1 5.02944 5.02944 1 10 1H22C26.9706 1 31 5.02944 31 10V32C31 33.6569 29.6569 35 28 35H4C2.34315 35 1 33.6569 1 32V10Z"
/>
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,23 @@ import * as React from "react";
import cx from "clsx";

import { TYPES } from "../../consts";
import type { SeatVariantProps } from "../../types";
import type { SeatVariantProps, SeatStatus } from "../../types";

type Props = {
fontSize: number;
} & SeatVariantProps;

const Text = ({ selected, type, label, fontSize }: Props) => {
const Text = ({ selected, status, type, label, fontSize }: Props) => {
const effectiveStatus = status ?? (selected ? "selected" : "default");
const isSelected = effectiveStatus === "selected";
const isProcessingOrDone = effectiveStatus === "processing" || effectiveStatus === "done";

return (
<text
className={cx(
selected && "fill-white-normal",
!selected && type === TYPES.LEGROOM && "fill-blue-dark",
!selected && type === TYPES.DEFAULT && "fill-product-dark",
(isSelected || isProcessingOrDone) && "fill-product-darker font-bold",
!isSelected && !isProcessingOrDone && type === TYPES.LEGROOM && "fill-blue-dark",
!isSelected && !isProcessingOrDone && type === TYPES.DEFAULT && "fill-product-dark",
)}
xmlSpace="preserve"
fontSize={fontSize}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,31 @@ import * as React from "react";
import cx from "clsx";

import { TYPES } from "../../consts";
import type { Type } from "../../types";
import type { Type, SeatStatus } from "../../types";

type Props = {
type: Type;
selected: boolean;
selected?: boolean;
status?: SeatStatus;
} & React.SVGProps<SVGPathElement> &
React.PropsWithChildren;

const TransitionPathFill = ({ children, type, selected, ...props }: Props) => {
const TransitionPathFill = ({ children, type, selected, status, ...props }: Props) => {
const effectiveStatus = status ?? (selected ? "selected" : "default");
const isSelected = effectiveStatus === "selected";
const isProcessingOrDone = effectiveStatus === "processing" || effectiveStatus === "done";

return (
<path
className={cx(
"duration-fast transition-colors ease-in",
type === TYPES.LEGROOM &&
(selected
(isSelected
? "fill-blue-normal group-focus-visible:fill-blue-normal-hover group-active:fill-blue-normal-hover"
: "fill-blue-light group-hover:fill-blue-light-hover group-focus-visible:fill-blue-light-active group-active:fill-blue-light-active"),
type === TYPES.DEFAULT &&
(selected
? "fill-product-normal group-focus-visible:fill-product-normal-hover group-active:fill-product-normal-hover"
(isSelected || isProcessingOrDone
? "fill-product-light-active group-focus-visible:fill-product-normal-hover group-active:fill-product-normal-hover"
: "fill-product-light group-hover:fill-product-light-hover group-focus-visible:fill-product-light-active group-active:fill-product-light-active"),
type === TYPES.UNAVAILABLE && "fill-cloud-light",
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,30 @@ import * as React from "react";
import cx from "clsx";

import { TYPES } from "../../consts";
import type { Type } from "../../types";
import type { Type, SeatStatus } from "../../types";

type Props = {
type: Type;
selected: boolean;
selected?: boolean;
status?: SeatStatus;
} & React.SVGProps<SVGPathElement> &
React.PropsWithChildren;

const TransitionPathStroke = ({ children, type, selected, ...props }: Props) => {
const TransitionPathStroke = ({ children, type, selected, status, ...props }: Props) => {
const effectiveStatus = status ?? (selected ? "selected" : "default");
const isSelected = effectiveStatus === "selected";
const isProcessingOrDone = effectiveStatus === "processing" || effectiveStatus === "done";

return (
<path
className={cx(
"duration-fast transition-colors ease-in",
type === TYPES.LEGROOM &&
(selected
(isSelected
? "stroke-blue-normal group-focus-visible:stroke-blue-normal-hover group-active:stroke-blue-normal-hover"
: "stroke-blue-light-active"),
type === TYPES.DEFAULT &&
(selected
(isSelected || isProcessingOrDone
? "stroke-product-normal group-focus-visible:stroke-product-normal-hover group-active:stroke-product-normal-hover"
: "stroke-product-light-active"),
type === TYPES.UNAVAILABLE && "stroke-cloud-light-active",
Expand Down
20 changes: 17 additions & 3 deletions packages/orbit-components/src/Seat/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type { Props } from "./types";
const Seat = ({
type = TYPES.DEFAULT,
selected = false,
status,
onClick,
size = SIZE_OPTIONS.MEDIUM,
dataTest,
Expand All @@ -26,6 +27,7 @@ const Seat = ({
description,
"aria-labelledby": ariaLabelledBy = "",
}: Props) => {
const effectiveStatus = status ?? (selected ? "selected" : "default");
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🐛 Possible Bug
The effectiveStatus calculation doesn't validate the status prop value, which could lead to invalid states being passed down to child components. The status should be validated against allowed values ('default', 'selected', 'processing', 'done') to prevent runtime errors.

Suggested change
const effectiveStatus = status ?? (selected ? "selected" : "default");
const validStatuses = ['default', 'selected', 'processing', 'done'] as const;
const effectiveStatus = status && validStatuses.includes(status) ? status : (selected ? 'selected' : 'default');

const randomId = useRandomIdSeed();
const titleId = title ? randomId("title") : "";
const descrId = description ? randomId("descr") : "";
Expand Down Expand Up @@ -55,12 +57,24 @@ const Seat = ({
{description && <desc id={descrId}>{description}</desc>}

{size === SIZE_OPTIONS.SMALL ? (
<SeatSmall type={type} selected={selected} label={label} />
<SeatSmall
type={type}
selected={effectiveStatus === "selected"}
status={effectiveStatus}
label={label}
/>
) : (
<SeatNormal type={type} selected={selected} label={label} />
<SeatNormal
type={type}
selected={effectiveStatus === "selected"}
status={effectiveStatus}
label={label}
/>
)}
</svg>
{selected && clickable && <SeatCircle size={size} type={type} />}
{effectiveStatus !== "default" && clickable && (
<SeatCircle size={size} type={type} status={effectiveStatus} />
)}
Comment on lines +75 to +77
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🐛 Possible Bug
The effectiveStatus is used in conditional rendering without type narrowing, which could lead to runtime errors if invalid status values are provided. The status check should be more explicit.

Suggested change
{effectiveStatus !== "default" && clickable && (
<SeatCircle size={size} type={type} status={effectiveStatus} />
)}
{(effectiveStatus === 'selected' || effectiveStatus === 'processing' || effectiveStatus === 'done') && clickable && (
<SeatCircle size={size} type={type} status={effectiveStatus} />
)}

</button>
{price && !(selected && type === TYPES.UNAVAILABLE) && (
<Text size="small" type="secondary">
Expand Down
8 changes: 7 additions & 1 deletion packages/orbit-components/src/Seat/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ import type * as Common from "../common/types";

export type Size = "small" | "medium";
export type Type = "default" | "legroom" | "unavailable";
export type SeatStatus = "default" | "selected" | "processing" | "done";

export interface SeatVariantProps {
selected: boolean;
selected?: boolean;
status?: SeatStatus;
type: Type;
label?: React.ReactNode;
}
Expand All @@ -21,7 +23,11 @@ export interface Props extends Common.Globals {
readonly description?: string;
readonly "aria-labelledby"?: string;
readonly onClick?: Common.Callback;
/**
* @deprecated Use `status="selected"` instead.
*/
readonly selected?: boolean;
readonly status?: SeatStatus;
readonly label?: string;
readonly price?: string;
}
Loading