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
120 changes: 120 additions & 0 deletions packages/components/src/components/Resizable/Resizable.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import {
Meta,
Story,
Props,
Status,
} from '../../../../../.storybook/components';

import * as Stories from './Resizable.stories';

<Meta of={Stories} />

# Resizable

<Status variant="experimental" />

Resizable changes an element's width and height using one or more composed handles.
The element stays in place while its size changes.

## Import

```tsx
import { Resizable } from '@koobiq/react-components';
```

## Usage

<Story of={Stories.Base} />

Both `Resizable` and `Resizable.Handle` render a `div` by default. Use the `as` prop
to render another element while preserving its native props and ref type.

```tsx
<Resizable as="section">
<Content />
<Resizable.Handle as="button" type="button" direction={[1, 1]} />
</Resizable>
```

## Props

<Props of={Stories.Base} />

## State

Use `defaultSize` for uncontrolled state, or combine `size` and `onResize` to control the size.
All values are measured in CSS pixels.

<Story of={Stories.Uncontrolled} />

### Intrinsic size

When both `size` and `defaultSize` are omitted, Resizable preserves its natural CSS size.
The measured size is fixed in pixels only after the first resize interaction.

<Story of={Stories.IntrinsicSize} />

## Direction

`Resizable.Handle` accepts a physical `[x, y]` direction. Each axis supports `-1`, `0`, and `1`:

- `x`: `-1` resizes from the left, `0` disables horizontal resizing, and `1` resizes from the right.
- `y`: `-1` resizes from the top, `0` disables vertical resizing, and `1` resizes from the bottom.
- `[0, 0]` is not a valid direction.

The direction remains physical in right-to-left layouts.

<Story of={Stories.SingleDirection} />

## Constraints

Use `minSize` and `maxSize` to constrain either dimension independently. The default minimum is zero,
and dimensions without a maximum are unbounded. If a minimum exceeds a maximum, the minimum wins.

## Custom handle

Handles can be styled without changing their hit area or resize behavior. This example adds a small
indicator outside the right edge and composes the handle with a tooltip. See
[Styling handles](#styling-handles) for available state attributes and [CSS Variables](#css-variables)
for geometry customization.

<Story of={Stories.CustomHandle} />

## Nested panels

Resizable components can be nested to build layouts with independently adjustable regions. In this
example, the outer handle changes the sidebar width and the inner handle changes the filters height.

<Story of={Stories.NestedPanels} />

## Keyboard interaction

Handles are focusable. Use the arrow keys to resize by one pixel, or hold <kbd>Shift</kbd> to resize
by ten pixels. Edge handles expose the ARIA separator pattern; corner handles expose an accessible
two-dimensional resize control.

## Disabled

Set `isDisabled` to remove all handles from the tab order and disable resize interactions.

<Story of={Stories.Disabled} />

## Styling handles

Handles provide positioning, hit areas, cursors, and state attributes, but no visible grip or line.
Use `className` and the `data-direction-x`, `data-direction-y`, `data-focus-visible`, `data-resizing`,
and `data-disabled` attributes to provide an application-specific indicator.

## CSS Variables

Use CSS variables to customize the handle geometry without inline styles.

| Variable |
| ---------------------------------- |
| `--kbq-resizable-handle-width` |
| `--kbq-resizable-handle-height` |
| `--kbq-resizable-handle-top` |
| `--kbq-resizable-handle-right` |
| `--kbq-resizable-handle-bottom` |
| `--kbq-resizable-handle-left` |
| `--kbq-resizable-handle-transform` |
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
.base {
position: relative;
box-sizing: border-box;
}

.handle {
--resizable-handle-width: var(--kbq-size-l);
--resizable-handle-height: var(--kbq-size-l);
--resizable-handle-top: auto;
--resizable-handle-right: auto;
--resizable-handle-bottom: auto;
--resizable-handle-left: auto;
--resizable-handle-translate-x: 0;
--resizable-handle-translate-y: 0;

position: absolute;
z-index: 1;
box-sizing: border-box;
inline-size: var(--kbq-resizable-handle-width, var(--resizable-handle-width));
block-size: var(
--kbq-resizable-handle-height,
var(--resizable-handle-height)
);
inset: var(--kbq-resizable-handle-top, var(--resizable-handle-top))
var(--kbq-resizable-handle-right, var(--resizable-handle-right))
var(--kbq-resizable-handle-bottom, var(--resizable-handle-bottom))
var(--kbq-resizable-handle-left, var(--resizable-handle-left));
touch-action: none;
background: transparent;
transform: var(
--kbq-resizable-handle-transform,
translate(
var(--resizable-handle-translate-x),
var(--resizable-handle-translate-y)
)
);
}

.handle[data-direction-x='-1'] {
--resizable-handle-left: 0;
--resizable-handle-translate-x: -50%;
}

.handle[data-direction-x='0'] {
--resizable-handle-width: 100%;
--resizable-handle-left: 0;

cursor: ns-resize;
}

.handle[data-direction-x='1'] {
--resizable-handle-right: 0;
--resizable-handle-translate-x: 50%;
}

.handle[data-direction-y='-1'] {
--resizable-handle-top: 0;
--resizable-handle-translate-y: -50%;
}

.handle[data-direction-y='0'] {
--resizable-handle-height: 100%;
--resizable-handle-top: 0;

cursor: ew-resize;
}

.handle[data-direction-y='1'] {
--resizable-handle-bottom: 0;
--resizable-handle-translate-y: 50%;
}

.handle:is(
[data-direction-x='-1'][data-direction-y='-1'],
[data-direction-x='1'][data-direction-y='1']
) {
cursor: nwse-resize;
}

.handle:is(
[data-direction-x='1'][data-direction-y='-1'],
[data-direction-x='-1'][data-direction-y='1']
) {
cursor: nesw-resize;
}

.handle[data-disabled] {
cursor: default;
}
Loading
Loading