fix: update form
This commit is contained in:
1
src/shared/lib/clickOutside/index.ts
Normal file
1
src/shared/lib/clickOutside/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { useClickOutside } from './useClickOutside';
|
||||
28
src/shared/lib/clickOutside/useClickOutside.tsx
Normal file
28
src/shared/lib/clickOutside/useClickOutside.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { RefObject, useEffect } from 'react';
|
||||
|
||||
export function useClickOutside(
|
||||
ref: RefObject<HTMLDivElement | null>,
|
||||
fn: (event: Event) => void,
|
||||
) {
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: Event) => {
|
||||
const el = ref?.current;
|
||||
if (event instanceof MouseEvent && window !== undefined) {
|
||||
const x = event?.offsetX || 0;
|
||||
const width = window?.innerWidth - 18;
|
||||
if (x >= width) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!el || el.contains((event?.target as Node) || null)) {
|
||||
return;
|
||||
}
|
||||
fn(event);
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, [ref, fn]);
|
||||
}
|
||||
15
src/shared/lib/detectIOS/detectIOS.ts
Normal file
15
src/shared/lib/detectIOS/detectIOS.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
function detectIOS() {
|
||||
const iosQuirkPresent = () => {
|
||||
const audio = new Audio();
|
||||
audio.volume = 0.5;
|
||||
return audio.volume === 1; // volume cannot be changed from "1" on iOS 12 and below
|
||||
};
|
||||
|
||||
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
|
||||
const isAppleDevice = navigator.userAgent.includes('Macintosh');
|
||||
const isTouchScreen = navigator.maxTouchPoints >= 1; // true for iOS 13 (and hopefully beyond)
|
||||
|
||||
return isIOS || (isAppleDevice && (isTouchScreen || iosQuirkPresent()));
|
||||
}
|
||||
|
||||
export { detectIOS };
|
||||
1
src/shared/lib/detectIOS/index.ts
Normal file
1
src/shared/lib/detectIOS/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { detectIOS } from './detectIOS';
|
||||
2
src/shared/lib/index.ts
Normal file
2
src/shared/lib/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { useClickOutside } from './clickOutside';
|
||||
export { detectIOS } from './detectIOS';
|
||||
@@ -1,39 +0,0 @@
|
||||
import s from './advancedPhoneInput.module.scss';
|
||||
import { DetailedHTMLProps, forwardRef, InputHTMLAttributes, Ref } from 'react';
|
||||
import { clsx } from 'clsx';
|
||||
import { Button, Input } from '@shared/ui';
|
||||
|
||||
type AdvancedPhoneInputProps = {
|
||||
containerClassName?: string;
|
||||
inputClassName?: string;
|
||||
buttonClassName?: string;
|
||||
onClick?: () => void;
|
||||
text: string;
|
||||
} & DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>;
|
||||
|
||||
const AdvancedPhoneInput = forwardRef(function AdvancedPhoneInput(
|
||||
{
|
||||
containerClassName,
|
||||
inputClassName,
|
||||
buttonClassName,
|
||||
onClick,
|
||||
text,
|
||||
...props
|
||||
}: AdvancedPhoneInputProps,
|
||||
ref: Ref<HTMLInputElement>,
|
||||
) {
|
||||
return (
|
||||
<div className={clsx(containerClassName, s.Container)}>
|
||||
<Input {...props} ref={ref} className={clsx(inputClassName, s.Phone)} />
|
||||
<Button
|
||||
className={clsx(buttonClassName, s.Button)}
|
||||
onClick={onClick}
|
||||
variant='orange'
|
||||
>
|
||||
{text}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default AdvancedPhoneInput;
|
||||
@@ -1,41 +0,0 @@
|
||||
.Container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.Phone {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
|
||||
@include iftablet{
|
||||
}
|
||||
@include iflaptop{
|
||||
padding-left: rem(16px);
|
||||
}
|
||||
@include ifdesktop{
|
||||
padding-left: rem(32px);
|
||||
}
|
||||
}
|
||||
|
||||
.Button {
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
right: rem(2px);
|
||||
top: calc(50% - 1.2rem);
|
||||
min-height: calc(100% - .25rem);
|
||||
|
||||
@include iftablet{
|
||||
top: calc(50% - 1.2rem);
|
||||
right: rem(3px);
|
||||
}
|
||||
|
||||
@include iflaptop{
|
||||
top: calc(50% - 1.4rem);
|
||||
right: rem(4px);
|
||||
}
|
||||
|
||||
@include ifdesktop{
|
||||
top: calc(50% - 1.6rem);
|
||||
right: rem(2px);
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { default as AdvancedPhoneInput } from './advanced-phone-input';
|
||||
@@ -1,6 +1,5 @@
|
||||
export { Button } from './button';
|
||||
export { Mark } from './mark';
|
||||
export { Input } from './input';
|
||||
export { AdvancedPhoneInput } from './advanced-phone-input';
|
||||
export { TextArea } from './text-area';
|
||||
export { PhoneInput } from './phone-input';
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
.Container {
|
||||
position: relative;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.Input {
|
||||
display: flex;
|
||||
background: $color-white;
|
||||
@@ -13,16 +18,16 @@
|
||||
color: $color-text;
|
||||
width: max-content;
|
||||
|
||||
@include iftablet{
|
||||
@include iftablet {
|
||||
font-size: rem(18px);
|
||||
}
|
||||
|
||||
@include iflaptop{
|
||||
@include iflaptop {
|
||||
font-size: rem(20px);
|
||||
padding: rem(10px) rem(20px);
|
||||
}
|
||||
|
||||
@include ifdesktop{
|
||||
@include ifdesktop {
|
||||
font-size: rem(24px);
|
||||
}
|
||||
|
||||
@@ -45,7 +50,7 @@
|
||||
border-color: $color-error;
|
||||
}
|
||||
|
||||
&_fullWidth{
|
||||
&_fullWidth {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -68,4 +73,16 @@
|
||||
transition: border-color ease .5s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.Error {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
left: rem(8px);
|
||||
bottom: rem(-16px);
|
||||
font-family: $font-open-sans;
|
||||
font-weight: $font-light;
|
||||
font-size: rem(12px);
|
||||
line-height: 100%;
|
||||
color: $color-error;
|
||||
}
|
||||
@@ -11,6 +11,7 @@ type InputProps = {
|
||||
fullWidth?: boolean;
|
||||
variant?: 'default' | 'ghost';
|
||||
error?: string | boolean;
|
||||
errorTextColor?: string;
|
||||
} & DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>;
|
||||
|
||||
const Input = forwardRef(function Input(
|
||||
@@ -19,22 +20,30 @@ const Input = forwardRef(function Input(
|
||||
fullWidth = false,
|
||||
variant = 'default',
|
||||
error = false,
|
||||
errorTextColor,
|
||||
...props
|
||||
}: InputProps,
|
||||
ref: Ref<HTMLInputElement>,
|
||||
) {
|
||||
return (
|
||||
<input
|
||||
{...props}
|
||||
ref={ref}
|
||||
className={clsx(
|
||||
s.Input,
|
||||
s['Input_' + variant],
|
||||
fullWidth && s.Input_fullWidth,
|
||||
error && s.Input_error,
|
||||
className,
|
||||
<div className={s.Container}>
|
||||
<input
|
||||
{...props}
|
||||
ref={ref}
|
||||
className={clsx(
|
||||
s.Input,
|
||||
s['Input_' + variant],
|
||||
fullWidth && s.Input_fullWidth,
|
||||
error && s.Input_error,
|
||||
className,
|
||||
)}
|
||||
/>
|
||||
{error && (
|
||||
<span className={s.Error} style={{ color: errorTextColor }}>
|
||||
{error}
|
||||
</span>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
1
src/shared/ui/modal/index.ts
Normal file
1
src/shared/ui/modal/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { Modal, ModalContent } from './modal';
|
||||
144
src/shared/ui/modal/modal.module.scss
Normal file
144
src/shared/ui/modal/modal.module.scss
Normal file
@@ -0,0 +1,144 @@
|
||||
.ModalBackdrop {
|
||||
z-index: 1000;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
padding: 0;
|
||||
|
||||
@include iftablet {
|
||||
position: fixed;
|
||||
height: 100vh;
|
||||
padding: 0 rem(20px);
|
||||
}
|
||||
|
||||
@include iflaptop {
|
||||
padding: 0 rem(48px);
|
||||
}
|
||||
animation: fadeIn ease 0.3s;
|
||||
|
||||
&_open {
|
||||
animation: fadeOut ease 0.3s;
|
||||
}
|
||||
|
||||
&_isIOS {
|
||||
height: 150vh !important;
|
||||
|
||||
@include iftablet {
|
||||
height: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ModalContent {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
background: white;
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
|
||||
//max-height: 90%;
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
//border-radius: 8px;
|
||||
animation: fadeIn ease 0.3s;
|
||||
|
||||
@include iftablet {
|
||||
position: relative;
|
||||
top: unset;
|
||||
bottom: unset;
|
||||
left: unset;
|
||||
right: unset;
|
||||
height: 100%;
|
||||
border-radius: rem(8px);
|
||||
}
|
||||
|
||||
&_open {
|
||||
animation: fadeOut ease 0.3s;
|
||||
}
|
||||
|
||||
&_isIOS {
|
||||
height: 100vh !important;
|
||||
|
||||
@include iftablet {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&__Inner {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
height: fit-content;
|
||||
}
|
||||
}
|
||||
|
||||
.Modal {
|
||||
position: relative;
|
||||
|
||||
&__Close {
|
||||
position: absolute;
|
||||
top: rem(5px);
|
||||
right: rem(5px);
|
||||
width: rem(36px);
|
||||
height: rem(36px);
|
||||
padding: rem(10px);
|
||||
cursor: pointer;
|
||||
margin-bottom: rem(4px);
|
||||
transform: rotate(0deg);
|
||||
transition: transform 0.3s;
|
||||
|
||||
@include iftablet {
|
||||
top: rem(35px);
|
||||
right: rem(35px);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@include iflaptop {
|
||||
top: rem(50px);
|
||||
right: rem(50px);
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
transform: rotate(45deg);
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: rem(17px);
|
||||
height: rem(17px);
|
||||
}
|
||||
|
||||
//svg {
|
||||
// transform: rotate(0deg);
|
||||
// transition: transform 0.3s;
|
||||
// &:hover,
|
||||
// &:active {
|
||||
// transform: rotate(45deg);
|
||||
// transition: transform 0.3s;
|
||||
// }
|
||||
//}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeOut {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
109
src/shared/ui/modal/modal.tsx
Normal file
109
src/shared/ui/modal/modal.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
'use client';
|
||||
|
||||
import s from './modal.module.scss';
|
||||
import { ReactNode, useEffect, useRef } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import clsx from 'clsx';
|
||||
import { useModal } from '@core/providers/modal-provider';
|
||||
import { detectIOS, useClickOutside } from '@shared/lib';
|
||||
|
||||
interface ModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const Modal = ({ children, isOpen, onClose }: ModalProps) => {
|
||||
useEffect(() => {
|
||||
const html = document.documentElement;
|
||||
if (isOpen) {
|
||||
html.style.overflow = 'hidden';
|
||||
}
|
||||
return () => {
|
||||
html.style.overflow = '';
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const isIOS = detectIOS();
|
||||
|
||||
return ReactDOM.createPortal(
|
||||
<div
|
||||
className={clsx(
|
||||
s.ModalBackdrop,
|
||||
isOpen && s.ModalBackdrop__open,
|
||||
isIOS && s.ModalBackdrop_isIOS,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>,
|
||||
document.body,
|
||||
);
|
||||
};
|
||||
|
||||
function ModalContent(props: {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
innerClassName?: string;
|
||||
closeByClickOutside?: boolean;
|
||||
}) {
|
||||
const {
|
||||
children,
|
||||
className,
|
||||
innerClassName,
|
||||
closeByClickOutside = false,
|
||||
} = props;
|
||||
|
||||
const modal = useModal();
|
||||
|
||||
const hideModal = () => modal.hideModal();
|
||||
|
||||
const modalRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const handleClickOutside = () => {
|
||||
if (closeByClickOutside) {
|
||||
hideModal();
|
||||
}
|
||||
};
|
||||
|
||||
useClickOutside(modalRef, handleClickOutside);
|
||||
|
||||
const isIOS = detectIOS();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(className, s.ModalContent, isIOS && s.ModalContent_isIOS)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
ref={modalRef}
|
||||
>
|
||||
<div className={clsx(s.ModalContent__Inner, innerClassName)}>
|
||||
<div className={s.Modal__Close} onClick={hideModal}>
|
||||
<CloseIcon />
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { Modal, ModalContent };
|
||||
|
||||
const CloseIcon = () => (
|
||||
<svg
|
||||
width='17'
|
||||
height='17'
|
||||
viewBox='0 0 17 17'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
>
|
||||
<path
|
||||
d='M2.5 15L15.5 2M2.5 2L15.5 15'
|
||||
stroke='#053635'
|
||||
strokeOpacity='0.3'
|
||||
strokeWidth='2'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -1,13 +1,15 @@
|
||||
//import s from './phone-input.module.scss';
|
||||
import { Input } from '@shared/ui';
|
||||
import { useMaskito } from '@maskito/react';
|
||||
|
||||
import { maskitoPhoneOptionsGenerator } from '@maskito/phone';
|
||||
import metadata from 'libphonenumber-js/min/metadata';
|
||||
import { DetailedHTMLProps, InputHTMLAttributes } from 'react';
|
||||
|
||||
type PhoneInput = {
|
||||
className?: string;
|
||||
variant?: 'default' | 'ghost';
|
||||
error?: string | boolean;
|
||||
errorTextColor?: string;
|
||||
fullWidth?: boolean;
|
||||
} & DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>;
|
||||
|
||||
export default function PhoneInput({ ...props }: PhoneInput) {
|
||||
@@ -17,9 +19,5 @@ export default function PhoneInput({ ...props }: PhoneInput) {
|
||||
});
|
||||
const maskedInputRef = useMaskito({ options });
|
||||
|
||||
return (
|
||||
<>
|
||||
<Input {...props} ref={maskedInputRef} type='tel' />
|
||||
</>
|
||||
);
|
||||
return <Input {...props} ref={maskedInputRef} type='tel' />;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
.Container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
@@ -6,7 +7,7 @@
|
||||
/* Allows the `resize` property to work on the div */
|
||||
overflow: hidden;
|
||||
|
||||
&_fullWidth{
|
||||
&_fullWidth {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -29,16 +30,16 @@
|
||||
width: 100%;
|
||||
flex-grow: 1;
|
||||
|
||||
@include iftablet{
|
||||
@include iftablet {
|
||||
font-size: rem(18px);
|
||||
}
|
||||
|
||||
@include iflaptop{
|
||||
@include iflaptop {
|
||||
font-size: rem(20px);
|
||||
padding: rem(10px) rem(20px);
|
||||
}
|
||||
|
||||
@include ifdesktop{
|
||||
@include ifdesktop {
|
||||
font-size: rem(24px);
|
||||
}
|
||||
|
||||
@@ -47,27 +48,33 @@
|
||||
/* Arrow mouse cursor over the scrollbar */
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: rem(12px);
|
||||
height: rem(12px);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track,
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-clip: content-box;
|
||||
border: rem(4px) solid transparent;
|
||||
border-radius: rem(12px);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background-color: #333; // цвет дорожки
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: #999; // цвет указателя
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
:where(&:not(:disabled))::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
&:where(:autofill) {
|
||||
/* Reliably removes native autofill colors */
|
||||
background-clip: text;
|
||||
@@ -113,4 +120,18 @@
|
||||
transition: border-color ease .5s;
|
||||
}
|
||||
}
|
||||
|
||||
&_error {
|
||||
border: 1px solid $color-error;
|
||||
}
|
||||
}
|
||||
|
||||
.Error {
|
||||
margin-left: 8px;
|
||||
margin-top: 4px;
|
||||
font-family: $font-open-sans;
|
||||
font-weight: $font-light;
|
||||
font-size: rem(12px);
|
||||
line-height: 100%;
|
||||
color: $color-error;
|
||||
}
|
||||
@@ -13,6 +13,8 @@ type TextAreaProps = {
|
||||
children?: ReactNode;
|
||||
variant?: 'default' | 'ghost';
|
||||
fullWidth?: boolean;
|
||||
error?: string | boolean;
|
||||
errorTextColor?: string;
|
||||
} & DetailedHTMLProps<
|
||||
TextareaHTMLAttributes<HTMLTextAreaElement>,
|
||||
HTMLTextAreaElement
|
||||
@@ -24,6 +26,8 @@ const TextArea = forwardRef(function TextArea(
|
||||
children,
|
||||
variant = 'default',
|
||||
fullWidth = false,
|
||||
error = false,
|
||||
errorTextColor,
|
||||
...props
|
||||
}: TextAreaProps,
|
||||
ref: Ref<HTMLTextAreaElement>,
|
||||
@@ -33,10 +37,20 @@ const TextArea = forwardRef(function TextArea(
|
||||
<textarea
|
||||
{...props}
|
||||
ref={ref}
|
||||
className={clsx(className, s.Area, s['Area_' + variant])}
|
||||
className={clsx(
|
||||
className,
|
||||
s.Area,
|
||||
s['Area_' + variant],
|
||||
error && s.Area_error,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</textarea>
|
||||
{error && (
|
||||
<span className={s.Error} style={{ color: errorTextColor }}>
|
||||
{error}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user