Skip to content
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
9 changes: 9 additions & 0 deletions component.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -1016,6 +1016,15 @@
"role"
]
},
{
"id": "overlays/sheet",
"name": null,
"category": "overlays",
"sourcePath": "src/tedi/components/overlays/sheet",
"status": [],
"description": null,
"keyProps": []
},
{
"id": "misc/separator/separator",
"name": "Separator",
Expand Down
24 changes: 24 additions & 0 deletions skills/tedi-react/references/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,12 @@ files. This is the part of this document worth maintaining by hand.
- **`OptionContent` is a template, not an item.** It has no role, click or focus handling by design.
It must go inside an interactive parent (`DropdownItem`, a `Select` option) that owns the role,
selection and keyboard handling.
- **`Sheet` is the bottom sheet; `Modal` is the centred dialog.** Reach for `Sheet` on mobile-first
surfaces — it anchors to the bottom and adds a drag handle, snap points and collapse-to-header that
`Modal` has no concept of. It is bottom-only by design (no side/drawer variant). `SheetModal` is a
*separate*, Modal-backed variant used internally by `CardStepper` and
`TableOfContents.Collapsible`; it is **not** a sub-component of `Sheet`, so don't reach for it when
composing one.
- **`TableCard` is `Table`'s readable mobile form.** Below a breakpoint, render a list of
`TableCard` (each row a stacked `<dl>` of label / value pairs with its own title / status /
actions) instead of collapsing columns; swap via `useBreakpoint` + `isBreakpointBelow(bp, 'md')`.
Expand All @@ -147,6 +153,24 @@ files. This is the part of this document worth maintaining by hand.
prop. Children win when both are given.
- **`TableOfContents.Item` children must be direct children.** Don't wrap them in another
component. Pass `underline={false}` on the `Link` inside an item to match the design.
- **`Sheet` is a compound with an auto-wired title.** Compose `Sheet.Trigger` / `.Content` /
`.Header` / `.Body` / `.Footer` / `.Closer`. `Sheet.Header`'s `title` is wired to the dialog's
`aria-labelledby` for you; set `Sheet.Content`'s `aria-label` only when there is no visible title.
- **`Sheet`'s `keepMounted` preserves state, invisibly.** By default the panel is removed from the
DOM when closed; `keepMounted` keeps it mounted-but-`hidden`, so form values and scroll position
survive a close→reopen. Nothing in the type signals that state-preservation difference — and the
kept panel is inert while closed, not just off-screen.
- **`Sheet` `snapPoints` resize the panel; they don't translate it.** With `snapPoints={[0.4, 0.9]}`
the bottom sheet's height *is* the active snap, so the body scrolls and the footer stays visible at
every rest position, and dragging below the lowest snap dismisses. `collapsible` is the lighter
relative: it peeks the sheet down to just its header (toggled by the `CollapseButton`). From that
peek the drag handle is direction-sensitive — drag up to expand, swipe down to dismiss.
- **`Sheet.Content`'s `radius` drives both the panel and header corners.** `radius="card" | "none" |
"default"` overrides the top-corner radius via a single `--tedi-sheet-radius` variable the panel
and header both read, so it's a sheet-level concern, not a `Sheet.Header` prop — and the panel's
`overflow: hidden` clips to its own radius, so a header-only override would be invisible anyway.
It's breakpoint-aware (`radius="none" md={{ radius: 'card' }}`); for any value outside the three
keywords, set `--tedi-sheet-radius` through `style`.
- **`Checkbox` / `Radio` pair with `Checkbox.Group` / `Radio.Group`.** The `.Group` owns selection
(`value` / `onChange`) and the shared props (`variant`, `size`, `name`); items inside inherit
`name` and auto-generate their `id`, so don't hand-wire those. `variant="card"` + `layout`
Expand Down
6 changes: 3 additions & 3 deletions src/tedi/components/navigation/card-stepper/card-stepper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useLabels } from '../../../providers/label-provider';
import { Icon } from '../../base/icon/icon';
import { Text } from '../../base/typography/text/text';
import { Button } from '../../buttons/button/button';
import { Sheet } from '../../overlays/sheet/sheet';
import { SheetModal } from '../../overlays/sheet/components/sheet-modal/sheet-modal';
import { VerticalStepper } from '../vertical-stepper/vertical-stepper';
import styles from './card-stepper.module.scss';
import { CardStepperStep, CardStepperStepProps } from './card-stepper-step';
Expand Down Expand Up @@ -342,7 +342,7 @@ const CardStepperInner = forwardRef<HTMLDivElement, CardStepperProps>((props, re
{current.bottomSlot && <div className={styles['tedi-card-stepper__bottom-slot']}>{current.bottomSlot}</div>}

{showStepList && (
<Sheet open={modalOpen} onToggle={setModalOpen} title={resolvedLabels.modalHeading}>
<SheetModal open={modalOpen} onToggle={setModalOpen} title={resolvedLabels.modalHeading}>
<VerticalStepper aria-label={resolvedLabels.modalHeading}>
{steps.map((step, index) => {
const navigable = isStepNavigable(index);
Expand Down Expand Up @@ -386,7 +386,7 @@ const CardStepperInner = forwardRef<HTMLDivElement, CardStepperProps>((props, re
);
})}
</VerticalStepper>
</Sheet>
</SheetModal>
)}
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useEffect, useMemo, useState } from 'react';
import { useLabels } from '../../../../../providers/label-provider';
import { Text } from '../../../../base/typography/text/text';
import { CollapseButton } from '../../../../buttons/collapse-button/collapse-button';
import { Sheet } from '../../../../overlays/sheet/sheet';
import { SheetModal } from '../../../../overlays/sheet/components/sheet-modal/sheet-modal';
import {
buildActiveTrail,
childrenToNodes,
Expand Down Expand Up @@ -125,7 +125,7 @@ export const TableOfContentsCollapsible = (props: TableOfContentsCollapsibleProp
<CollapseButton open={open} onOpenChange={setOpen} underline={false} aria-haspopup="dialog" />
</div>

<Sheet
<SheetModal
open={open}
onToggle={setOpen}
ariaLabel={title}
Expand All @@ -147,7 +147,7 @@ export const TableOfContentsCollapsible = (props: TableOfContentsCollapsibleProp
>
<TableOfContentsList nodes={nodes} heading={null} />
</div>
</Sheet>
</SheetModal>
</TableOfContentsContext.Provider>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import cn from 'classnames';
import { ReactNode } from 'react';

import styles from '../../sheet.module.scss';
import { useSheetContext } from '../../sheet-context';

export type SheetBodyPadding = 'default' | 'none';

export interface SheetBodyProps {
/**
* Body content. Scrolls when it overflows the available height.
*/
children: ReactNode;
/**
* Body padding.
* - `default` — the standard sheet padding.
* - `none` — edge-to-edge content. Use when the content brings its own padding
* (e.g. `Tabs`, a full-width action list) and should sit flush with the sheet edges.
* @default default
*/
padding?: SheetBodyPadding;
/**
* Additional class name.
*/
className?: string;
}

export const SheetBody = ({ children, padding = 'default', className }: SheetBodyProps): JSX.Element | null => {
const { collapsed } = useSheetContext();

if (collapsed) return null;

return (
<div
className={cn(
styles['tedi-sheet__body'],
{ [styles['tedi-sheet__body--no-padding']]: padding === 'none' },
className
)}
>
{children}
</div>
);
};

SheetBody.displayName = 'Sheet.Body';
export default SheetBody;
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { cloneElement, MouseEvent, ReactElement } from 'react';

import { useSheetContext } from '../../sheet-context';

export interface SheetCloserProps {
/**
* Element that closes the sheet when clicked. The element is cloned and receives
* a wrapped `onClick` that calls the original handler first, then closes the sheet.
*/
children: ReactElement<{ onClick?: (event: MouseEvent<HTMLElement>) => void }>;
}

export const SheetCloser = ({ children }: SheetCloserProps): JSX.Element => {
const { onOpenChange } = useSheetContext();
return cloneElement(children, {
onClick: (event: MouseEvent<HTMLElement>) => {
children.props.onClick?.(event);
onOpenChange(false);
},
});
};

SheetCloser.displayName = 'Sheet.Closer';
export default SheetCloser;
Loading
Loading