Skip to content

Commit

Permalink
feat(Drawer): resizer, portal rendering, hide veil
Browse files Browse the repository at this point in the history
  • Loading branch information
StepanKirichenko committed Jul 17, 2024
1 parent aeb2acb commit 904ab2e
Show file tree
Hide file tree
Showing 14 changed files with 665 additions and 38 deletions.
54 changes: 54 additions & 0 deletions src/components/Drawer/Drawer.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@
$block: '.#{variables.$ns}drawer';

#{$block} {
pointer-events: none;

--_--resizer-color: var(--g-color-base-generic);
--_--resizer-handle-color: var(--g-color-line-generic);
--_--resizer-handle-color-hover: var(--g-color-line-generic-hover);
--_--resizer-z-index: 100;

&__item {
position: absolute;
left: 0;
Expand All @@ -11,6 +18,7 @@ $block: '.#{variables.$ns}drawer';
height: 100%;
will-change: transform;
background-color: var(--g-color-base-background);
pointer-events: initial;

&_direction_right {
left: auto;
Expand Down Expand Up @@ -64,8 +72,13 @@ $block: '.#{variables.$ns}drawer';

&__veil {
position: absolute;
pointer-events: initial;
inset: 0;
background-color: var(--g-color-sfx-veil);

&_hidden {
display: none;
}
}

&__veil-transition-enter {
Expand All @@ -89,4 +102,45 @@ $block: '.#{variables.$ns}drawer';
&__veil-transition-exit-done {
visibility: hidden;
}

&__resizer-handle {
width: 2px;
height: 28px;

background: var(--gn-drawer-item-resizer-handle-color, var(--_--resizer-handle-color));
border-radius: 2px;
}

&__resizer {
position: absolute;
top: 0;
z-index: var(--gn-drawer-item-resizer-z-index, var(--_--resizer-z-index));

display: flex;
flex-direction: column;
justify-content: center;
align-items: center;

width: 8px;
height: 100%;

cursor: col-resize;

background: var(--gn-drawer-item-resizer-color, var(--_--resizer-color));

&_direction_right {
left: 0;
}

&_direction_left {
right: 0;
}
}

&__resizer:hover &__resizer-handle {
background: var(
--gn-drawer-item-resizer-handle-color-hover,
var(--_--resizer-handle-color-hover)
);
}
}
126 changes: 98 additions & 28 deletions src/components/Drawer/Drawer.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import React from 'react';

import {useBodyScrollLock} from '@gravity-ui/uikit';
import {Portal, useBodyScrollLock, useForkRef} from '@gravity-ui/uikit';
import {CSSTransition, Transition} from 'react-transition-group';

import {block} from '../utils/cn';

import {type DrawerDirection, useResizableDrawerItem} from './utils';

import './Drawer.scss';

const b = block('drawer');
const TIMEOUT = 300;

export type DrawerDirection = 'right' | 'left';

export interface DrawerItemProps {
/** Unique identifier for the drawer item. */
id: string;
Expand All @@ -35,32 +35,82 @@ export interface DrawerItemProps {

/** Additional custom class name applied to the drawer item. */
className?: string;
}

export const DrawerItem: React.FC<DrawerItemProps> = ({
visible,
content,
children,
direction = 'left',
className,
}) => {
const itemRef = React.useRef<HTMLDivElement>(null);
const cssDirection = direction === 'left' ? undefined : direction;
/** Determines whether the drawer item can be resized */
resizable?: boolean;

return (
<CSSTransition
in={visible}
timeout={TIMEOUT}
unmountOnExit
classNames={b('item-transition', {direction: cssDirection})}
nodeRef={itemRef}
>
<div ref={itemRef} className={b('item', {direction: cssDirection}, className)}>
{children ?? content}
/**
* The width of the resizable drawer item.
* If not provided, the width will be stored internally.
*/
width?: number;

/**
* Called at the end of resizing. Can be used to save the new width.
* @param width The new width of the drawer item
*/
onResize?: (width: number) => void;

/** The minimum width of the resizable drawer item */
minResizeWidth?: number;

/** The maximum width of the resizable drawer item */
maxResizeWidth?: number;
}

export const DrawerItem = React.forwardRef<HTMLDivElement, DrawerItemProps>(
function DrawerItem(props, ref) {
const {
visible,
content,
children,
direction = 'left',
className,
resizable,
width,
minResizeWidth,
maxResizeWidth,
onResize,
} = props;

const itemRef = React.useRef<HTMLDivElement>(null);
const handleRef = useForkRef(ref, itemRef);
const cssDirection = direction === 'left' ? undefined : direction;

const {resizedWidth, resizerHandlers} = useResizableDrawerItem({
direction,
width,
minResizeWidth,
maxResizeWidth,
onResize,
});

const resizerElement = resizable ? (
<div className={b('resizer', {direction})} {...resizerHandlers}>
<div className={b('resizer-handle')} />
</div>
</CSSTransition>
);
};
) : null;

return (
<CSSTransition
in={visible}
timeout={TIMEOUT}
unmountOnExit
classNames={b('item-transition', {direction: cssDirection})}
nodeRef={itemRef}
>
<div
ref={handleRef}
className={b('item', {direction: cssDirection}, className)}
style={{width: resizable ? `${resizedWidth}px` : undefined}}
>
{resizerElement}
{children ?? content}
</div>
</CSSTransition>
);
},
);

type DrawerChild = React.ReactElement<DrawerItemProps>;

Expand All @@ -85,6 +135,12 @@ export interface DrawerProps {

/** Optional callback function that is called when the escape key is pressed, if the drawer is open. */
onEscape?: () => void;

/** Optional flag to hide the background darkening */
hideVeil?: boolean;

/** Optional flag to not use `Portal` for drawer */
disablePortal?: boolean;
}

export const Drawer: React.FC<DrawerProps> = ({
Expand All @@ -94,6 +150,8 @@ export const Drawer: React.FC<DrawerProps> = ({
onVeilClick,
onEscape,
preventScrollBody = true,
hideVeil,
disablePortal,
}) => {
let someItemVisible = false;
React.Children.forEach(children, (child) => {
Expand Down Expand Up @@ -124,7 +182,7 @@ export const Drawer: React.FC<DrawerProps> = ({
const containerRef = React.useRef<HTMLDivElement>(null);
const veilRef = React.useRef<HTMLDivElement>(null);

return (
const drawer = (
<Transition
in={someItemVisible}
timeout={{enter: 0, exit: TIMEOUT}}
Expand All @@ -143,7 +201,11 @@ export const Drawer: React.FC<DrawerProps> = ({
classNames={b('veil-transition')}
nodeRef={veilRef}
>
<div ref={veilRef} className={b('veil')} onClick={onVeilClick} />
<div
ref={veilRef}
className={b('veil', {hidden: hideVeil})}
onClick={onVeilClick}
/>
</CSSTransition>
{React.Children.map(children, (child) => {
if (
Expand All @@ -163,4 +225,12 @@ export const Drawer: React.FC<DrawerProps> = ({
}}
</Transition>
);

if (disablePortal) {
return drawer;
}

return <Portal>{drawer}</Portal>;
};

export type {DrawerDirection} from './utils';
48 changes: 40 additions & 8 deletions src/components/Drawer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,20 +32,50 @@ const App = () => {
export default App;
```

An example of a resizable drawer item

```tsx
import React from 'react';
import {Drawer, DrawerItem} from '@gravity-ui/navigation';

const App = () => {
const [isVisible, setVisible] = React.useState(false);
const [width, setWidth] = React.useState(400);

return (
<div>
<button onClick={() => setVisible(true)}>Open Drawer</button>
<Drawer onEscape={() => setVisible(false)} onVeilClick={() => setVisible(false)}>
<DrawerItem id="item1" visible={isVisible} resizable width={width} onResize={setWidth}>
<p>Content of the drawer</p>
</DrawerItem>
</Drawer>
</div>
);
};

export default App;
```

## Components

The Drawer module consists of two primary components: `Drawer` and `DrawerItem`.

### `DrawerItem` Props

| Name | Description | Type | Default |
| :-------- | :-------------------------------------------------------------------------------------- | :---------------: | :-----: |
| id | Unique identifier for the drawer item. | `string` | |
| children | Content to be displayed within the drawer item, preferable over the deprecated content. | `React.ReactNode` | |
| content | (deprecated) use children. Content to be displayed within the drawer item. | `React.ReactNode` | |
| visible | Determines whether the drawer item is visible or hidden. | `boolean` | |
| direction | Specifies the direction from which the drawer should slide in (left or right). | `DrawerDirection` | `left` |
| className | HTML `class` attribute | `string` | |
| Name | Description | Type | Default |
| :------------- | :-------------------------------------------------------------------------------------- | :-----------------------: | :-----: |
| id | Unique identifier for the drawer item. | `string` | |
| children | Content to be displayed within the drawer item, preferable over the deprecated content. | `React.ReactNode` | |
| content | (deprecated) use children. Content to be displayed within the drawer item. | `React.ReactNode` | |
| visible | Determines whether the drawer item is visible or hidden. | `boolean` | |
| direction | Specifies the direction from which the drawer should slide in (left or right). | `DrawerDirection` | `left` |
| className | HTML `class` attribute | `string` | |
| resizable | Determines whether the drawer item can be resized | `boolean` | |
| width | The width of the resizable drawer item | `number` | |
| onResize | Callback function called at the end of resizing. Can be used to save the new width. | `(width: number) => void` | |
| minResizeWidth | The minimum width of the resizable drawer item | `number` | |
| maxResizeWidth | The maximum width of the resizable drawer item | `number` | |

### `Drawer` Props

Expand All @@ -57,3 +87,5 @@ The Drawer module consists of two primary components: `Drawer` and `DrawerItem`.
| style | Optional inline styles to be applied to the drawer component. | `React.CSSProperties` | |
| onVeilClick | Optional callback function that is called when the veil (overlay) is clicked. | `(event: React.MouseEvent) => void` | |
| onEscape | Optional callback function that is called when the escape key is pressed, if the drawer is open. | `() => void` | |
| hideVeil | Optional flag to hide the background darkening | `boolean` | |
| disablePortal | Optional flag to hide the background darkening | `boolean` | |
42 changes: 42 additions & 0 deletions src/components/Drawer/__stories__/DisablePortal.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
.disable-portal {
width: 100dvw;
height: 100dvh;

display: flex;
flex-direction: column;

&__header {
width: 100%;
padding: 20px;
border-bottom: 1px solid var(--g-color-line-generic);
display: flex;
align-items: center;
gap: 8px;
}

&__container {
position: relative;
padding: 16px;
width: 600px;
height: 400px;
border: 1px solid var(--g-color-line-brand);
}

&__drawer {
position: absolute;
inset: 0;
overflow: hidden;
}

&__item {
width: 400px;
height: 100%;
overflow-x: hidden;
}

&__item-content {
padding: 8px 16px;
height: 100%;
overflow-y: scroll;
}
}
Loading

0 comments on commit 904ab2e

Please sign in to comment.