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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
*.log
.idea
.DS_Store
.vscode
node_modules
.cache
dist
Expand Down
1 change: 1 addition & 0 deletions components/overlay/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
src
48 changes: 48 additions & 0 deletions components/overlay/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# `@byndyusoft-ui/overlay`

The Overlay component creates a semi-transparent layer over content.

## Installation

```bash
npm install @byndyusoft-ui/overlay
```

## Usage

```tsx
import { Overlay } from '@byndyusoft-ui/overlay';

function Example() {
return (
<div>
<div>Main content</div>
<Overlay isVisible={true} color="#000000" blur={5} center>
<div>Overlay content</div>
</Overlay>
</div>
);
}
```

## Props

| Prop | Type | Default | Description |
| ----------------- | ------------------ | ------------------ | ------------------------------------------------------------------------- |
| children | ReactNode | - | Content to be rendered inside the overlay |
| className | string | - | Additional CSS class for the overlay container |
| classNames | IOverlayClassNames | Overlay.module.css | Object with class names for overlay elements |
| refElement | HTMLElement | - | Reference to the element that will be used lock scroll |
| color | string | #000000 | Overlay background color (hex format) |
| backgroundOpacity | number | 0.6 | Background opacity (from 0 to 1) |
| blur | number | 10 | Blur effect value in pixels |
| zIndex | number | 100 | Overlay z-index |
| isVisible | boolean | false | Controls overlay visibility |
| center | boolean | false | Centers the content inside overlay |
| fixed | boolean | false | Determines whether overlay should have fixed position instead of absolute |

# Notes

- Component uses `useBodyScrollLock` hook to prevent body scrolling when overlay is visible
- The color prop expects a hex color value (e.g., "#000000")
- The refElement prop is used to lock scroll of the element when overlay is visible
39 changes: 39 additions & 0 deletions components/overlay/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"name": "@byndyusoft-ui/overlay",
"version": "0.1.0",
"description": "Byndyusoft UI Overlay React Component",
"keywords": [
"byndyusoft",
"byndyusoft-ui",
"react",
"overlay"
],
"author": "Dmitrii Maletskii <maleckij@byndyusoft.com>",
"homepage": "https://github.com/Byndyusoft/ui/tree/master/components/overlay#readme",
"license": "ISC",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"repository": {
"type": "git",
"url": "git+https://github.com/Byndyusoft/ui.git"
},
"scripts": {
"build": "rollup --config",
"clean": "rimraf dist",
"lint": "eslint src --config ../../eslint.config.js",
"test": "jest --config ../../jest.config.js --roots components/overlay/src"
},
"bugs": {
"url": "https://github.com/Byndyusoft/ui/issues"
},
"publishConfig": {
"access": "public"
},
"dependencies": {
"@byndyusoft-ui/use-scroll-lock": "^0.1.0"
},
"peerDependencies": {
"react": ">=17",
"react-dom": ">=17"
}
}
29 changes: 29 additions & 0 deletions components/overlay/rollup.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import typescript from '@rollup/plugin-typescript';
import baseConfig from '../../rollup.base.config';
import postcss from 'rollup-plugin-postcss';

export default {
...baseConfig,
input: ['src/index.ts'],
plugins: [
...baseConfig.plugins,
typescript({
tsconfig: './tsconfig.json',
module: 'ESNext',
exclude: [
'src/**/*.stories.tsx',
'src/**/*.tests.tsx',
'src/**/__stories__/**',
'src/**/__tests__/**',
'node_modules/**'
]
}),
postcss({
modules: true,
extract: false,
minimize: true,
sourceMap: true,
inject: true
})
]
};
41 changes: 41 additions & 0 deletions components/overlay/src/Overlay.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
.container {
position: fixed;
inset: 0;
}

.center > * {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}

.container.fadeIn {
animation: fadeIn 0.4s ease forwards;
}

.container.fadeOut {
animation: fadeOut 0.4s ease forwards;
}

@keyframes fadeIn {
0% {
display: block;
opacity: 0;
}
100% {
display: block;
opacity: 1;
}
}

@keyframes fadeOut {
0% {
display: block;
opacity: 1;
}
100% {
display: none;
opacity: 0;
}
}
111 changes: 111 additions & 0 deletions components/overlay/src/Overlay.tests.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { userEvent } from '@testing-library/user-event';
import Overlay from './Overlay';

describe('components/Overlay', () => {
test('renders visible correctly', () => {
render(<Overlay isVisible>Test</Overlay>);

expect(screen.getByRole('presentation')).toHaveClass('fadeIn');
expect(screen.getByText('Test')).toBeInTheDocument();
});

test('renders invisible', () => {
render(<Overlay isVisible={false}>Test</Overlay>);

expect(screen.getByRole('presentation')).not.toHaveClass('isVisible');
expect(screen.queryByText('Test')).not.toBeInTheDocument();
});

test('handles click', async () => {
const onClick = jest.fn();

render(
<Overlay isVisible={false} onClick={onClick}>
Test
</Overlay>
);

await userEvent.click(screen.getByRole('presentation'));

expect(onClick).toBeCalledTimes(1);
});

test('renders with custom className', () => {
render(
<Overlay isVisible className="custom-class">
Test
</Overlay>
);

expect(screen.getByRole('presentation')).toHaveClass('custom-class');
});

test('renders with custom data-testid', () => {
render(
<Overlay isVisible data-testid="custom-overlay">
Test
</Overlay>
);

expect(screen.getByTestId('custom-overlay')).toBeInTheDocument();
});

test('applies center prop correctly', () => {
render(
<Overlay isVisible center>
Test
</Overlay>
);

expect(screen.getByRole('presentation')).toHaveClass('center');
});

test('applies fixed position when fixed prop is true', () => {
render(
<Overlay isVisible fixed>
Test
</Overlay>
);

expect(screen.getByRole('presentation')).toHaveStyle({ position: 'fixed' });
});

test('applies custom styles correctly', () => {
render(
<Overlay isVisible color="#FF0000" backgroundOpacity={0.8} fixed zIndex={200}>
Test
</Overlay>
);

const overlay = screen.getByRole('presentation');
const expectedStyles = {
zIndex: 200,
backgroundColor: 'rgba(255, 0, 0, 0.8)',
position: 'fixed'
};

Object.entries(expectedStyles).forEach(([property, value]) => {
expect(overlay).toHaveStyle({ [property]: value });
});
});

test('applies custom classNames', () => {
const customClassNames = {
container: 'custom-container',
fadeIn: 'custom-fade-in',
fadeOut: 'custom-fade-out',
center: 'custom-center'
} as const;

render(
<Overlay isVisible classNames={customClassNames}>
Test
</Overlay>
);

const overlay = screen.getByRole('presentation');
expect(overlay).toHaveClass('custom-container', 'custom-fade-in');
});
});
55 changes: 55 additions & 0 deletions components/overlay/src/Overlay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import React, { forwardRef } from 'react';
import cn from 'classnames';
import { useScrollLock } from '@byndyusoft-ui/use-scroll-lock';
import { getDefaultOverlayClassNames, hexToRgba } from './utilities';
import { IOverlayProps } from './Overlay.types';

const Overlay = forwardRef<HTMLDivElement, IOverlayProps>(
(
{
children,
className,
classNames,
refElement,
color = '#000000',
backgroundOpacity = 0.6,
blur = 10,
zIndex = 100,
isVisible = false,
center = false,
fixed = false,
...props
},
ref
): JSX.Element => {
useScrollLock(isVisible, refElement);
const mergedClassNames = Object.assign(getDefaultOverlayClassNames(), classNames);

return (
<div
className={cn(
mergedClassNames.container,
isVisible ? mergedClassNames.fadeIn : mergedClassNames.fadeOut,
center && mergedClassNames.center,
className
)}
style={{
zIndex,
backgroundColor: hexToRgba(color, backgroundOpacity),
backdropFilter: `blur(${blur}px)`,
position: fixed ? 'fixed' : 'absolute'
}}
role="presentation"
ref={ref}
tabIndex={-1}
{...props}
>
{children && isVisible && children}
</div>
);
}
);

Overlay.displayName = 'Overlay';

export default Overlay;
20 changes: 20 additions & 0 deletions components/overlay/src/Overlay.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { HTMLAttributes } from 'react';

export interface IOverlayProps extends HTMLAttributes<HTMLDivElement> {
classNames?: IOverlayClassNames;
refElement?: HTMLElement | null;
color?: string;
backgroundOpacity?: number;
blur?: number;
zIndex?: number;
isVisible?: boolean;
center?: boolean;
fixed?: boolean;
}

export interface IOverlayClassNames {
container?: string;
fadeIn?: string;
fadeOut?: string;
center?: string;
}
1 change: 1 addition & 0 deletions components/overlay/src/__stories__/Loading.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
34 changes: 34 additions & 0 deletions components/overlay/src/__stories__/Overlay.docs.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Meta, Markdown, Canvas, Source, ArgsTable } from '@storybook/blocks';
import Readme from '../../README.md';
import Overlay from '../Overlay';
import * as OverlayStories from './Overlay.stories';

<Meta title="components/Overlay" of={OverlayStories} />

<Markdown>{Readme}</Markdown>

## Usage

To use the component in your project you must:

1. Import the component where you need it:

<Source language="javascript" code="import Overlay from '@byndyusoft-ui/overlay';" />

2. Call the component and give it the desired props, for example:

- Overlay view with centered modal

<Canvas sourceState="shown" of={OverlayStories.OverlayStoryWithModal} />

- Overlay view with custom props

<Canvas sourceState="shown" of={OverlayStories.OverlayStoryWithCustomProps} />

- Overlay view with custom styles

<Canvas sourceState="shown" of={OverlayStories.OverlayStoryWithCustomStyles} />

- Overlay inside block

<Canvas sourceState="shown" of={OverlayStories.OverlayStoryWithLoading} />
Loading