From 5fef4bc80411873f8948abfe7811a8c55bb17360 Mon Sep 17 00:00:00 2001 From: Fedor Katurov <gotham48@gmail.com> Date: Wed, 13 Oct 2021 16:14:37 +0700 Subject: [PATCH] added flow display controls overlay --- .env.development | 4 +- src/components/common/MenuDots/index.tsx | 17 ++++ src/components/flow/FlowCell/index.tsx | 26 ++++- .../flow/FlowCell/styles.module.scss | 48 ++++++++++ src/components/flow/FlowCellMenu/index.tsx | 79 +++++++-------- .../flow/FlowCellMenu/styles.module.scss | 81 ++++++++-------- src/components/flow/FlowGrid/index.tsx | 2 +- src/components/input/Toggle/index.tsx | 17 +++- .../input/Toggle/styles.module.scss | 18 +++- src/sprites/Sprites.tsx | 95 ++++++++++++++++--- src/styles/variables.scss | 9 ++ src/utils/hooks/flow/useFlowCellControls.ts | 12 +-- src/utils/hooks/useClickOutsideFocus.ts | 30 ++++++ src/utils/types.ts | 5 + 14 files changed, 327 insertions(+), 116 deletions(-) create mode 100644 src/components/common/MenuDots/index.tsx create mode 100644 src/utils/hooks/useClickOutsideFocus.ts diff --git a/.env.development b/.env.development index 589f28a1..cf60c666 100644 --- a/.env.development +++ b/.env.development @@ -1,3 +1,3 @@ #REACT_APP_API_HOST=http://localhost:3334/ -REACT_APP_API_HOST=https://pig.vault48.org/ -REACT_APP_REMOTE_CURRENT=https://pig.vault48.org/static/ +REACT_APP_API_HOST=https://pig.staging.vault48.org/ +REACT_APP_REMOTE_CURRENT=https://pig.staging.vault48.org/static/ diff --git a/src/components/common/MenuDots/index.tsx b/src/components/common/MenuDots/index.tsx new file mode 100644 index 00000000..09671742 --- /dev/null +++ b/src/components/common/MenuDots/index.tsx @@ -0,0 +1,17 @@ +import React, { FC } from 'react'; +import styles from '~/components/flow/FlowCell/styles.module.scss'; +import { Icon } from '~/components/input/Icon'; +import { ButtonProps } from '~/utils/types'; +import classNames from 'classnames'; + +interface Props extends ButtonProps {} + +const MenuDots: FC<Props> = ({ ...rest }) => ( + <button {...rest} className={classNames(styles.button, rest.className)}> + <div className={styles.dots}> + <Icon icon="menu" size={24} /> + </div> + </button> +); + +export { MenuDots }; diff --git a/src/components/flow/FlowCell/index.tsx b/src/components/flow/FlowCell/index.tsx index 8e908ee1..107b21e8 100644 --- a/src/components/flow/FlowCell/index.tsx +++ b/src/components/flow/FlowCell/index.tsx @@ -1,4 +1,4 @@ -import React, { FC } from 'react'; +import React, { FC, useState } from 'react'; import styles from './styles.module.scss'; import { NavLink } from 'react-router-dom'; import { CellShade } from '~/components/flow/CellShade'; @@ -8,6 +8,10 @@ import { FlowCellText } from '~/components/flow/FlowCellText'; import classNames from 'classnames'; import { FlowCellMenu } from '~/components/flow/FlowCellMenu'; import { useFlowCellControls } from '~/utils/hooks/flow/useFlowCellControls'; +import { Icon } from '~/components/input/Icon'; +import { useFocusEvent } from '~/utils/hooks/useFocusEvent'; +import { useClickOutsideFocus } from '~/utils/hooks/useClickOutsideFocus'; +import { MenuDots } from '~/components/common/MenuDots'; interface Props { id: INode['id']; @@ -29,10 +33,11 @@ const FlowCell: FC<Props> = ({ flow, text, title, - canEdit, + canEdit = false, onChangeCellView, }) => { - const withText = ((!!flow.display && flow.display !== 'single') || !image) && !!text; + const withText = + ((!!flow.display && flow.display !== 'single') || !image) && flow.show_description && !!text; const { hasDescription, setViewHorizontal, @@ -41,12 +46,22 @@ const FlowCell: FC<Props> = ({ setViewSingle, toggleViewDescription, } = useFlowCellControls(id, text, flow, onChangeCellView); + const { isActive: isMenuActive, activate, ref, deactivate } = useClickOutsideFocus(); return ( - <div className={classNames(styles.cell, styles[flow.display || 'single'])}> - {canEdit && ( + <div className={classNames(styles.cell, styles[flow.display || 'single'])} ref={ref as any}> + {canEdit && !isMenuActive && ( <div className={styles.menu}> + <MenuDots onClick={activate} /> + </div> + )} + + {canEdit && isMenuActive && ( + <div className={styles.display_modal}> <FlowCellMenu + onClose={deactivate} + currentView={flow.display} + descriptionEnabled={flow.show_description} hasDescription={hasDescription} setViewHorizontal={setViewHorizontal} setViewQuadro={setViewQuadro} @@ -56,6 +71,7 @@ const FlowCell: FC<Props> = ({ /> </div> )} + <NavLink className={styles.link} to={to}> {withText && ( <FlowCellText className={styles.text} heading={<h4 className={styles.title}>{title}</h4>}> diff --git a/src/components/flow/FlowCell/styles.module.scss b/src/components/flow/FlowCell/styles.module.scss index 370d061d..57b14f20 100644 --- a/src/components/flow/FlowCell/styles.module.scss +++ b/src/components/flow/FlowCell/styles.module.scss @@ -107,3 +107,51 @@ flex-direction: column-reverse; } } + +.display_modal { + @include appear; + + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 11; +} + +.button { + width: 48px; + height: 48px; + display: flex; + align-items: flex-start; + justify-content: flex-end; + fill: white; + padding: 7px; + box-sizing: border-box; + cursor: pointer; +} + +.dots { + @include blur($content_bg, 5px, 0.7); + + padding: 5px 0 0 0; + background: $content_bg; + border-radius: $radius; + width: 18px; + height: 30px; + position: relative; + + svg { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + + opacity: 0.5; + transition: opacity 0.25s; + + :hover > & { + opacity: 1; + } + } +} diff --git a/src/components/flow/FlowCellMenu/index.tsx b/src/components/flow/FlowCellMenu/index.tsx index dd6a7874..78cc8b32 100644 --- a/src/components/flow/FlowCellMenu/index.tsx +++ b/src/components/flow/FlowCellMenu/index.tsx @@ -1,12 +1,15 @@ import React, { FC } from 'react'; import styles from './styles.module.scss'; import { Icon } from '~/components/input/Icon'; -import { Manager, Popper, Reference } from 'react-popper'; -import { useFocusEvent } from '~/utils/hooks/useFocusEvent'; import classNames from 'classnames'; -import { usePopperModifiers } from '~/utils/hooks/usePopperModifiers'; +import { Toggle } from '~/components/input/Toggle'; +import { Group } from '~/components/containers/Group'; +import { FlowDisplayVariant } from '~/redux/types'; interface Props { + onClose: () => void; + currentView: FlowDisplayVariant; + descriptionEnabled: boolean; hasDescription: boolean; toggleViewDescription: () => void; setViewSingle: () => void; @@ -16,51 +19,51 @@ interface Props { } const FlowCellMenu: FC<Props> = ({ + onClose, hasDescription, toggleViewDescription, + descriptionEnabled, setViewSingle, setViewHorizontal, setViewVertical, setViewQuadro, }) => { - const { onFocus, onBlur, focused } = useFocusEvent(); - const modifiers = usePopperModifiers(0, 10); - return ( - <Manager> - <button className={styles.button} onFocus={onFocus} onBlur={onBlur}> - <Reference> - {({ ref }) => ( - <div className={styles.dots} ref={ref}> - <Icon icon="menu" size={24} /> - </div> - )} - </Reference> - </button> + <div className={classNames(styles.dropdown)}> + {onClose && ( + <button className={styles.close} onClick={onClose} type="button"> + <Icon icon="close" size={24} /> + </button> + )} - <Popper placement="bottom" strategy="fixed" modifiers={modifiers}> - {({ ref, style }) => ( - <div - ref={ref} - style={style} - className={classNames(styles.dropdown, { [styles.active]: focused })} - > - <div className={styles.menu}> - {hasDescription && ( - <> - <Icon icon="text" onMouseDown={toggleViewDescription} size={32} /> - <div className={styles.sep} /> - </> - )} - <Icon icon="cell-single" onMouseDown={setViewSingle} size={32} /> - <Icon icon="cell-double-h" onMouseDown={setViewHorizontal} size={32} /> - <Icon icon="cell-double-v" onMouseDown={setViewVertical} size={32} /> - <Icon icon="cell-quadro" onMouseDown={setViewQuadro} size={32} /> - </div> - </div> + <div className={styles.menu}> + <div className={styles.display}> + <Icon icon="cell-single" onMouseDown={setViewSingle} size={48} /> + <Icon + icon={descriptionEnabled ? 'cell-double-h-text' : 'cell-double-h'} + onMouseDown={setViewHorizontal} + size={48} + /> + <Icon + icon={descriptionEnabled ? 'cell-double-v-text' : 'cell-double-v'} + onMouseDown={setViewVertical} + size={48} + /> + <Icon + icon={descriptionEnabled ? 'cell-quadro-text' : 'cell-quadro'} + onMouseDown={setViewQuadro} + size={48} + /> + </div> + + {hasDescription && ( + <Group className={styles.description} horizontal onClick={toggleViewDescription}> + <Toggle color="white" value={descriptionEnabled} /> + <span>Текст</span> + </Group> )} - </Popper> - </Manager> + </div> + </div> ); }; diff --git a/src/components/flow/FlowCellMenu/styles.module.scss b/src/components/flow/FlowCellMenu/styles.module.scss index ffd5da55..b59f7318 100644 --- a/src/components/flow/FlowCellMenu/styles.module.scss +++ b/src/components/flow/FlowCellMenu/styles.module.scss @@ -1,53 +1,16 @@ @import "~/styles/variables"; -.button { - width: 48px; - height: 48px; - display: flex; - align-items: flex-start; - justify-content: flex-end; - fill: white; - padding: 5px; - box-sizing: border-box; - cursor: pointer; -} - -.dots { - @include blur($content_bg, 5px, 0.7); - - padding: 5px 0 0 0; - background: $content_bg; - border-radius: $radius; - width: 20px; - height: 32px; - position: relative; - - svg { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - - opacity: 0.5; - transition: opacity 0.25s; - - :hover > & { - opacity: 1; - } - } -} - .dropdown { - @include dropdown_shadow; + @include outer_shadow; @include blur($red, 15px, 0.3); + width: 100%; + height: 100%; border-radius: $radius; padding: $gap; - visibility: hidden; - - &.active { - visibility: visible; - } + display: flex; + align-items: center; + justify-content: center; } .menu { @@ -67,3 +30,35 @@ @include outer_shadow; height: 1px; } + +.display { + display: grid; + grid-template-columns: repeat(2, 1fr); + grid-row-gap: $gap; + grid-column-gap: $gap; +} + +.description { + margin-top: $gap; + font: $font_12_semibold; + text-transform: uppercase; + display: flex; + flex-direction: row; + padding: 0 4px; + + span { + flex: 1; + text-align: right; + } +} + +.close { + position: absolute; + top: 0; + right: 0; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; +} diff --git a/src/components/flow/FlowGrid/index.tsx b/src/components/flow/FlowGrid/index.tsx index 6af28ff2..4bcdf6ef 100644 --- a/src/components/flow/FlowGrid/index.tsx +++ b/src/components/flow/FlowGrid/index.tsx @@ -30,7 +30,7 @@ export const FlowGrid: FC<IProps> = ({ user, nodes, onChangeCellView }) => { to={URLS.NODE_URL(node.id)} image={getURLFromString(node.thumbnail, PRESETS.cover)} flow={node.flow} - text={node.flow.show_description ? node.description : ''} + text={node.description} title={node.title} canEdit={canEditNode(node, user)} onChangeCellView={onChangeCellView} diff --git a/src/components/input/Toggle/index.tsx b/src/components/input/Toggle/index.tsx index f36ee358..bb591012 100644 --- a/src/components/input/Toggle/index.tsx +++ b/src/components/input/Toggle/index.tsx @@ -1,16 +1,17 @@ import React, { FC, useCallback } from 'react'; import styles from './styles.module.scss'; import classNames from 'classnames'; +import { ButtonProps, DivProps } from '~/utils/types'; -type ToggleColor = 'primary' | 'secondary' | 'lab' | 'danger'; +type ToggleColor = 'primary' | 'secondary' | 'lab' | 'danger' | 'white'; -interface IProps { +type IProps = Omit<ButtonProps, 'value' | 'color'> & { value?: boolean; handler?: (val: boolean) => void; color?: ToggleColor; -} +}; -const Toggle: FC<IProps> = ({ value, handler, color = 'primary' }) => { +const Toggle: FC<IProps> = ({ value, handler, color = 'primary', ...rest }) => { const onClick = useCallback(() => { if (!handler) { return; @@ -21,8 +22,14 @@ const Toggle: FC<IProps> = ({ value, handler, color = 'primary' }) => { return ( <button + {...rest} type="button" - className={classNames(styles.toggle, { [styles.active]: value }, styles[color])} + className={classNames( + styles.toggle, + { [styles.active]: value }, + styles[color], + rest.className + )} onClick={onClick} /> ); diff --git a/src/components/input/Toggle/styles.module.scss b/src/components/input/Toggle/styles.module.scss index 00060eab..3f3edd42 100644 --- a/src/components/input/Toggle/styles.module.scss +++ b/src/components/input/Toggle/styles.module.scss @@ -12,6 +12,19 @@ cursor: pointer; position: relative; + &.white { + box-shadow: inset white 0 0 0 2px; + + &::after { + width: 14px; + height: 14px; + top: 5px; + left: 5px; + background: none; + box-shadow: inset white 0 0 0 2px; + } + } + &::after { content: ' '; position: absolute; @@ -26,7 +39,6 @@ } &.active { - &::after { transform: translate(24px, 0); background-color: white; @@ -47,5 +59,9 @@ &.danger { background-color: $red; } + + &.monochrome { + background-color: white; + } } } diff --git a/src/sprites/Sprites.tsx b/src/sprites/Sprites.tsx index 530ad045..b7e34144 100644 --- a/src/sprites/Sprites.tsx +++ b/src/sprites/Sprites.tsx @@ -14,27 +14,92 @@ const Sprites: FC = () => ( </pattern> </defs> - <g id="cell-single" stroke="none" transform="translate(2 2)"> - <path d="M0,0 L9,0 L9,9 L0,9 L0,0 Z" fill="url(#pattern_stripes)" /> - <path d="M11,0 L20,0 L20,9 L11,9 L11,0 Z M12,1 L12,8 L19,8 L19,1 L12,1 Z" /> - <path d="M11,11 L20,11 L20,20 L11,20 L11,11 Z M12,12 L12,19 L19,19 L19,12 L12,12 Z" /> - <path d="M0,11 L9,11 L9,20 L0,20 L0,11 Z M1,12 L1,19 L8,19 L8,12 L1,12 Z" /> + <g id="cell-single" stroke="none"> + <rect x="13.5" y="2.5" width="8" height="8" rx="1.5" stroke="currentColor" fill="none" /> + <rect x="13.5" y="13.5" width="8" height="8" rx="1.5" stroke="currentColor" fill="none" /> + <rect x="2.5" y="13.5" width="8" height="8" rx="1.5" stroke="currentColor" fill="none" /> + <path + fillRule="evenodd" + clipRule="evenodd" + d="M4 2C2.89543 2 2 2.89543 2 4V9C2 10.1046 2.89543 11 4 11H9C10.1046 11 11 10.1046 11 9V4C11 2.89543 10.1046 2 9 2H4ZM4 8H7C7.55228 8 8 8.44772 8 9C8 9.55228 7.55228 10 7 10H4C3.44772 10 3 9.55228 3 9C3 8.44772 3.44772 8 4 8Z" + fill="currentColor" + stroke="none" + /> </g> - <g id="cell-double-h" stroke="none" transform="translate(2 2)"> - <path d="M0,0 L19,0 L19,9 L0,9 L0,0 Z" fill="url(#pattern_stripes)" /> - <path d="M11,11 L20,11 L20,20 L11,20 L11,11 Z M12,12 L12,19 L19,19 L19,12 L12,12 Z" /> - <path d="M0,11 L9,11 L9,20 L0,20 L0,11 Z M1,12 L1,19 L8,19 L8,12 L1,12 Z" /> + <g id="cell-double-h" stroke="none"> + <rect x="13.5" y="13.5" width="8" height="8" rx="1.5" stroke="currentColor" fill="none" /> + <rect x="2.5" y="13.5" width="8" height="8" rx="1.5" stroke="currentColor" fill="none" /> + <path + fillRule="evenodd" + clipRule="evenodd" + d="M4 2C2.89543 2 2 2.89543 2 4V9C2 10.1046 2.89543 11 4 11H20C21.1046 11 22 10.1046 22 9V4C22 2.89543 21.1046 2 20 2H4ZM4 8C3.44772 8 3 8.44772 3 9C3 9.55228 3.44772 10 4 10H7C7.55228 10 8 9.55228 8 9C8 8.44772 7.55228 8 7 8H4Z" + fill="currentColor" + stroke="none" + /> </g> - <g id="cell-double-v" stroke="none" transform="translate(2 2)"> - <path d="M0,0 L9,0 L9,19 L0,19 L0,0 Z" fill="url(#pattern_stripes)" /> - <path d="M11,0 L20,0 L20,9 L11,9 L11,0 Z M12,1 L12,8 L19,8 L19,1 L12,1 Z" /> - <path d="M11,11 L20,11 L20,20 L11,20 L11,11 Z M12,12 L12,19 L19,19 L19,12 L12,12 Z" /> + <g id="cell-double-h-text" stroke="none"> + <rect x="13.5" y="13.5" width="8" height="8" rx="1.5" stroke="currentColor" fill="none" /> + <rect x="2.5" y="13.5" width="8" height="8" rx="1.5" stroke="currentColor" fill="none" /> + <path + fillRule="evenodd" + clipRule="evenodd" + d="M4 2C2.89543 2 2 2.89543 2 4V9C2 10.1046 2.89543 11 4 11H20C21.1046 11 22 10.1046 22 9V4C22 2.89543 21.1046 2 20 2H4ZM4 3C3.44772 3 3 3.44772 3 4V9C3 9.55228 3.44772 10 4 10H10C10.5523 10 11 9.55228 11 9V4C11 3.44772 10.5523 3 10 3H4Z" + fill="currentColor" + stroke="none" + /> + <rect x="4" y="4" width="5" height="1" rx="0.5" fill="currentColor" stroke="none" /> + <rect x="4" y="6" width="6" height="1" rx="0.5" fill="currentColor" stroke="none" /> + <rect x="4" y="8" width="6" height="1" rx="0.5" fill="currentColor" stroke="none" /> </g> - <g id="cell-quadro" stroke="none" transform="translate(2 2)"> - <path d="M0,0 L19,0 L19,19 L0,19 L0,0 Z" fill="url(#pattern_stripes)" /> + <g id="cell-double-v" stroke="none"> + <rect x="13.5" y="13.5" width="8" height="8" rx="1.5" stroke="currentColor" fill="none" /> + <rect x="13.5" y="2.5" width="8" height="8" rx="1.5" stroke="currentColor" fill="none" /> + <path + fillRule="evenodd" + clipRule="evenodd" + d="M4 2C2.89543 2 2 2.89543 2 4V20C2 21.1046 2.89543 22 4 22H9C10.1046 22 11 21.1046 11 20V4C11 2.89543 10.1046 2 9 2H4ZM4 19C3.44772 19 3 19.4477 3 20C3 20.5523 3.44772 21 4 21H7C7.55228 21 8 20.5523 8 20C8 19.4477 7.55228 19 7 19H4Z" + fill="currentColor" + stroke="none" + /> + </g> + <g id="cell-double-v-text" stroke="none"> + <rect x="13.5" y="13.5" width="8" height="8" rx="1.5" stroke="currentColor" fill="none" /> + <rect x="13.5" y="2.5" width="8" height="8" rx="1.5" stroke="currentColor" fill="none" /> + <path + fillRule="evenodd" + clipRule="evenodd" + d="M4 2C2.89543 2 2 2.89543 2 4V20C2 21.1046 2.89543 22 4 22H9C10.1046 22 11 21.1046 11 20V4C11 2.89543 10.1046 2 9 2H4ZM4 14C3.44772 14 3 14.4477 3 15V20C3 20.5523 3.44772 21 4 21H9C9.55228 21 10 20.5523 10 20V15C10 14.4477 9.55228 14 9 14H4Z" + fill="currentColor" + stroke="none" + /> + <rect x="4" y="15" width="3" height="1" rx="0.5" fill="currentColor" stroke="none" /> + <rect x="4" y="17" width="5" height="1" rx="0.5" fill="currentColor" stroke="none" /> + <rect x="4" y="19" width="5" height="1" rx="0.5" fill="currentColor" stroke="none" /> + </g> + + <g id="cell-quadro" stroke="none"> + <path + fillRule="evenodd" + clipRule="evenodd" + d="M4 2C2.89543 2 2 2.89543 2 4V20C2 21.1046 2.89543 22 4 22H20C21.1046 22 22 21.1046 22 20V4C22 2.89543 21.1046 2 20 2H4ZM4 19C3.44772 19 3 19.4477 3 20C3 20.5523 3.44772 21 4 21H12C12.5523 21 13 20.5523 13 20C13 19.4477 12.5523 19 12 19H4Z" + fill="currentColor" + stroke="none" + /> + </g> + <g id="cell-quadro-text" stroke="none"> + <path + fillRule="evenodd" + clipRule="evenodd" + d="M4 2C2.89543 2 2 2.89543 2 4V20C2 21.1046 2.89543 22 4 22H20C21.1046 22 22 21.1046 22 20V4C22 2.89543 21.1046 2 20 2H4ZM4 14C3.44772 14 3 14.4477 3 15V20C3 20.5523 3.44772 21 4 21H10C10.5523 21 11 20.5523 11 20V15C11 14.4477 10.5523 14 10 14H4Z" + fill="currentColor" + stroke="none" + /> + <rect x="4" y="15" width="4" height="1" rx="0.5" fill="currentColor" stroke="none" /> + <rect x="4" y="17" width="6" height="1" rx="0.5" fill="currentColor" stroke="none" /> + <rect x="4" y="19" width="6" height="1" rx="0.5" fill="currentColor" stroke="none" /> </g> <g id="play"> diff --git a/src/styles/variables.scss b/src/styles/variables.scss index 5dda5ddd..c511e7fb 100644 --- a/src/styles/variables.scss +++ b/src/styles/variables.scss @@ -312,3 +312,12 @@ $sidebar_border: transparentize(white, 0.95); grid-auto-rows: 50vw; } } + +@mixin appear { + @keyframes __appear { + from { opacity: 0; } + to { opacity: 1; } + } + + animation: __appear 0.25s forwards; +} diff --git a/src/utils/hooks/flow/useFlowCellControls.ts b/src/utils/hooks/flow/useFlowCellControls.ts index 68c78e2c..28124dfa 100644 --- a/src/utils/hooks/flow/useFlowCellControls.ts +++ b/src/utils/hooks/flow/useFlowCellControls.ts @@ -9,7 +9,7 @@ export const useFlowCellControls = ( ) => { const onChange = useCallback( (value: Partial<FlowDisplay>) => onChangeCellView(id, { ...flow, ...value }), - [] + [flow, onChangeCellView] ); const hasDescription = !!description && description.length > 32; @@ -17,23 +17,23 @@ export const useFlowCellControls = ( const toggleViewDescription = useCallback(() => { const show_description = !(flow && flow.show_description); onChange({ show_description }); - }, [id, flow, onChange]); + }, [flow, onChange]); const setViewSingle = useCallback(() => { onChange({ display: 'single' }); - }, [id, flow, onChange]); + }, [onChange]); const setViewHorizontal = useCallback(() => { onChange({ display: 'horizontal' }); - }, [id, flow, onChange]); + }, [onChange]); const setViewVertical = useCallback(() => { onChange({ display: 'vertical' }); - }, [id, flow]); + }, [onChange]); const setViewQuadro = useCallback(() => { onChange({ display: 'quadro' }); - }, [id, flow, onChange]); + }, [onChange]); return { hasDescription, diff --git a/src/utils/hooks/useClickOutsideFocus.ts b/src/utils/hooks/useClickOutsideFocus.ts new file mode 100644 index 00000000..6fcf0677 --- /dev/null +++ b/src/utils/hooks/useClickOutsideFocus.ts @@ -0,0 +1,30 @@ +/** + * Handles blur by detecting clicks outside refs. + */ +import { useCallback, useEffect, useRef, useState } from 'react'; + +export const useClickOutsideFocus = () => { + const ref = useRef<HTMLElement>(); + const [isActive, setIsActive] = useState(false); + + const activate = useCallback(() => setIsActive(true), [setIsActive]); + const deactivate = useCallback(() => setIsActive(false), [setIsActive]); + + useEffect(() => { + if (!isActive || !ref.current) { + return; + } + + const deactivator = (event: MouseEvent) => { + if (!ref.current?.contains(event.target as Node)) { + deactivate(); + } + }; + + document.addEventListener('mouseup', deactivator); + + return () => document.removeEventListener('mouseup', deactivator); + }, [isActive]); + + return { ref, isActive, activate, deactivate }; +}; diff --git a/src/utils/types.ts b/src/utils/types.ts index 4698ec0e..438d56d3 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -11,3 +11,8 @@ export type IMGProps = React.DetailedHTMLProps< React.ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement >; + +export type ButtonProps = React.DetailedHTMLProps< + React.ButtonHTMLAttributes<HTMLButtonElement>, + HTMLButtonElement +>;