Skip to content

Commit

Permalink
feat(ImageBasePositionedComponent): add subcomponent to positioning c…
Browse files Browse the repository at this point in the history
…omponent in Image (#7166)

* feat(ImageBasePositionedComponent): add subcomponent to positioning component in Image

- Добавил сабкомпонент ImageBasePositionedComponent для абсолютного позиционирования компонентов в компоненте Image.
- Добавил тесты для компонента
- Добавил стори для компонента
- Добавил использование компонента в документацию

* feat(ImageBasePositionedComponent): add placement to position component

* fix(ImageBasePositionedComponent): add horizontal and vertical indentation from image

* feat(ImageBasePositionedComponent): add 2xs and 4xl size of indent

* fix(ImageBasePositionedComponent): rewrite calculate indent logic

* fix(ImageBaseFloatElement): rename component

* fix(ImageBaseFloatElement): fix test

* fix: rename PositionedComponent to FloatElement

* fix(ImageBaseFloatElement): remove containerRef props and rename 'on-image-hover' to 'on-hover'

* fix(ImageBaseFloatElement): fix tests

* fix(ImageBaseFloatElement): rename props vertical and horizontal indent to block and inline

* fix(ImageBase): refactor onMouseOver/Out Subscription

* fix(ImageBase): fix types

* fix(ImageBaseFloatElement): rename prop position to placement and remove absolute positioning

* fix(ImageBaseFloatElement): remove BEM
  • Loading branch information
EldarMuhamethanov authored Dec 11, 2024
1 parent c370b70 commit 2789dd1
Show file tree
Hide file tree
Showing 12 changed files with 711 additions and 1 deletion.
4 changes: 4 additions & 0 deletions packages/vkui/src/components/Image/Image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ const getBorderRadiusBySizeInPx = (
export const Image: React.FC<ImageProps> & {
Badge: typeof ImageBadge;
Overlay: typeof ImageBase.Overlay;
FloatElement: typeof ImageBase.FloatElement;
} = ({
size = IMAGE_DEFAULT_SIZE,
borderRadius = 'm',
Expand Down Expand Up @@ -175,3 +176,6 @@ Image.Badge.displayName = 'Image.Badge';

Image.Overlay = ImageBase.Overlay;
Image.Overlay.displayName = 'Image.Overlay';

Image.FloatElement = ImageBase.FloatElement;
Image.FloatElement.displayName = 'Image.FloatElement';
82 changes: 82 additions & 0 deletions packages/vkui/src/components/Image/Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,52 @@ const OthersFeatures = () => {
);
};

const WithFloatElements = () => {
const [showContextMenu, setShowContextMenu] = useState(true);
const [contextMenuOpened, setContextMenuOpened] = useState(false);
const [contextMenuVisibility, setContextMenuVisibility] = useState('on-hover');

return (
<Group header={<Header mode="secondary">C позиционированными компонентами</Header>}>
<FormLayoutGroup mode="horizontal">
<FormItem top="Контекстное меню">
<Checkbox
checked={showContextMenu}
onChange={(e) => setShowContextMenu(e.target.checked)}
>
Показать контекстное меню
</Checkbox>
</FormItem>
<FormItem top="Контекстное меню">
<Select
options={[
{ label: 'Всегда', value: 'always' },
{ label: 'При наведении на картинку', value: 'on-hover' },
]}
value={contextMenuVisibility}
disabled={!showContextMenu}
onChange={(e) => setContextMenuVisibility(e.target.value)}
/>
</FormItem>
</FormLayoutGroup>
<Flex margin="auto" gap={'m'}>
<Image size={96} src={getAvatarUrl('app_shorm_online')} alt="Приложение шторм онлайн">
{showContextMenu && (
<Image.FloatElement
placement="top-end"
inlineIndent="l"
blockIndent="l"
visibility={contextMenuOpened ? 'always' : contextMenuVisibility}
>
<ContextMenu onShownChange={setContextMenuOpened} />
</Image.FloatElement>
)}
</Image>
</Flex>
</Group>
);
};

const Example = () => {
return (
<View activePanel="avatar">
Expand All @@ -85,6 +131,8 @@ const Example = () => {
<Default />
<Responsive />
<OthersFeatures />

<WithFloatElements />
</Panel>
</View>
);
Expand Down Expand Up @@ -203,5 +251,39 @@ const ImagePropsForm = ({ onBorderRadiusChange, onBadgeChange, onOverlayChange }
);
};

const ContextMenu = ({ onShownChange }) => {
return (
<Popover
noStyling
trigger="click"
role="dialog"
onShownChange={onShownChange}
content={({ onClose }) => (
<div
style={{
backgroundColor: 'var(--vkui--color_background_modal_inverse)',
borderRadius: 8,
boxShadow: '0 0 10px rgba(0, 0, 0, 0.3)',
}}
>
<CellButton role="menuitem" before={<Icon28AddOutline />} onClick={onClose}>
Добавить
</CellButton>
<CellButton
role="menuitem"
before={<Icon28DeleteOutline />}
mode="danger"
onClick={onClose}
>
Удалить
</CellButton>
</div>
)}
>
<Button mode="primary" after={<Icon16MoreHorizontal />}></Button>
</Popover>
);
};

<Example />;
```
41 changes: 40 additions & 1 deletion packages/vkui/src/components/ImageBase/ImageBase.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use client';

import { useRef } from 'react';
import * as React from 'react';
import { classNames } from '@vkontakte/vkjs';
import { useExternRef } from '../../hooks/useExternRef';
Expand All @@ -8,6 +9,12 @@ import { getFetchPriorityProp } from '../../lib/utils';
import type { AnchorHTMLAttributesOnly, HasRef, HasRootRef, LiteralUnion } from '../../types';
import { Clickable } from '../Clickable/Clickable';
import { ImageBaseBadge, type ImageBaseBadgeProps } from './ImageBaseBadge/ImageBaseBadge';
import {
type FloatElementIndentation,
type FloatElementPlacement,
ImageBaseFloatElement,
type ImageBaseFloatElementProps,
} from './ImageBaseFloatElement/ImageBaseFloatElement';
import { ImageBaseOverlay, type ImageBaseOverlayProps } from './ImageBaseOverlay/ImageBaseOverlay';
import { ImageBaseContext } from './context';
import type { ImageBaseContextProps, ImageBaseExpectedIconProps, ImageBaseSize } from './types';
Expand All @@ -20,6 +27,9 @@ export type {
ImageBaseBadgeProps,
ImageBaseOverlayProps,
ImageBaseContextProps,
ImageBaseFloatElementProps,
FloatElementPlacement,
FloatElementIndentation,
};

export {
Expand Down Expand Up @@ -125,6 +135,7 @@ const sizeToNumber = (size: number | string | undefined): number | undefined =>
export const ImageBase: React.FC<ImageBaseProps> & {
Badge: typeof ImageBaseBadge;
Overlay: typeof ImageBaseOverlay;
FloatElement: typeof ImageBaseFloatElement;
} = ({
alt,
crossOrigin,
Expand All @@ -150,16 +161,21 @@ export const ImageBase: React.FC<ImageBaseProps> & {
withTransparentBackground,
objectFit = 'cover',
keepAspectRatio = false,
getRootRef,
...restProps
}: ImageBaseProps) => {
const size = sizeProp ?? minOr([sizeToNumber(widthSize), sizeToNumber(heightSize)], defaultSize);
const wrapperRef = useExternRef(getRootRef);

const width = widthSize ?? (keepAspectRatio ? undefined : size);
const height = heightSize ?? (keepAspectRatio ? undefined : size);

const [loaded, setLoaded] = React.useState(false);
const [failed, setFailed] = React.useState(false);

const mouseOverHandlersRef = useRef<VoidFunction[]>([]);
const mouseOutHandlersRef = useRef<VoidFunction[]>([]);

const hasSrc = src || srcSet;
const needShowFallbackIcon = (failed || !hasSrc) && React.isValidElement(fallbackIconProp);

Expand Down Expand Up @@ -205,15 +221,35 @@ export const ImageBase: React.FC<ImageBaseProps> & {
[imgRef, loaded],
);

const onMouseOver = () => {
mouseOverHandlersRef.current.forEach((fn) => fn());
};

const onMouseOut = () => {
mouseOutHandlersRef.current.forEach((fn) => fn());
};

const contextValue = React.useMemo(
() => ({
size,
onMouseOverHandlers: mouseOverHandlersRef.current,
onMouseOutHandlers: mouseOutHandlersRef.current,
}),
[size],
);

return (
<ImageBaseContext.Provider value={{ size }}>
<ImageBaseContext.Provider value={contextValue}>
<Clickable
baseStyle={{ width, height }}
baseClassName={classNames(
styles.host,
loaded && styles.loaded,
withTransparentBackground && styles.transparentBackground,
)}
getRootRef={wrapperRef}
onMouseOver={onMouseOver}
onMouseOut={onMouseOut}
{...restProps}
>
{hasSrc && (
Expand Down Expand Up @@ -263,3 +299,6 @@ ImageBase.Badge.displayName = 'ImageBase.Badge';

ImageBase.Overlay = ImageBaseOverlay;
ImageBase.Overlay.displayName = 'ImageBase.Overlay';

ImageBase.FloatElement = ImageBaseFloatElement;
ImageBase.FloatElement.displayName = 'ImageBase.FloatElement';
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
.host {
position: absolute;
z-index: var(--vkui_internal--z_index_image_base_positioned_element);
transition: opacity 0.3s ease-in-out;

--vkui_internal--FloatElement_horizontal_indent: 0;
--vkui_internal--FloatElement_vertical_indent: 0;
}

.inlineIndent2xs {
--vkui_internal--FloatElement_horizontal_indent: var(--vkui--spacing_size_2xs);
}

.inlineIndentXs {
--vkui_internal--FloatElement_horizontal_indent: var(--vkui--spacing_size_xs);
}

.inlineIndentS {
--vkui_internal--FloatElement_horizontal_indent: var(--vkui--spacing_size_s);
}

.inlineIndentM {
--vkui_internal--FloatElement_horizontal_indent: var(--vkui--spacing_size_m);
}

.inlineIndentL {
--vkui_internal--FloatElement_horizontal_indent: var(--vkui--spacing_size_l);
}

.inlineIndentXl {
--vkui_internal--FloatElement_horizontal_indent: var(--vkui--spacing_size_xl);
}

.inlineIndent2xl {
--vkui_internal--FloatElement_horizontal_indent: var(--vkui--spacing_size_2xl);
}

.inlineIndent3xl {
--vkui_internal--FloatElement_horizontal_indent: var(--vkui--spacing_size_3xl);
}

.inlineIndent4xl {
--vkui_internal--FloatElement_horizontal_indent: var(--vkui--spacing_size_4xl);
}

.blockIndent2xs {
--vkui_internal--FloatElement_vertical_indent: var(--vkui--spacing_size_2xs);
}

.blockIndentXs {
--vkui_internal--FloatElement_vertical_indent: var(--vkui--spacing_size_xs);
}

.blockIndentS {
--vkui_internal--FloatElement_vertical_indent: var(--vkui--spacing_size_s);
}

.blockIndentM {
--vkui_internal--FloatElement_vertical_indent: var(--vkui--spacing_size_m);
}

.blockIndentL {
--vkui_internal--FloatElement_vertical_indent: var(--vkui--spacing_size_l);
}

.blockIndentXl {
--vkui_internal--FloatElement_vertical_indent: var(--vkui--spacing_size_xl);
}

.blockIndent2xl {
--vkui_internal--FloatElement_vertical_indent: var(--vkui--spacing_size_2xl);
}

.blockIndent3xl {
--vkui_internal--FloatElement_vertical_indent: var(--vkui--spacing_size_3xl);
}

.blockIndent4xl {
--vkui_internal--FloatElement_vertical_indent: var(--vkui--spacing_size_4xl);
}

.hidden {
opacity: 0;
}

.placementTopStart {
inset-inline-start: var(--vkui_internal--FloatElement_horizontal_indent);
inset-block-start: var(--vkui_internal--FloatElement_vertical_indent);
}

.placementTop {
inset-inline-start: 50%;
inset-block-start: var(--vkui_internal--FloatElement_vertical_indent);
transform: translateX(-50%);
}

.placementTopEnd {
inset-inline-end: var(--vkui_internal--FloatElement_horizontal_indent);
inset-block-start: var(--vkui_internal--FloatElement_vertical_indent);
}

.placementBottomStart {
inset-inline-start: var(--vkui_internal--FloatElement_horizontal_indent);
inset-block-end: var(--vkui_internal--FloatElement_vertical_indent);
}

.placementBottom {
inset-inline-start: 50%;
inset-block-end: var(--vkui_internal--FloatElement_vertical_indent);
transform: translateX(-50%);
}

.placementBottomEnd {
inset-block-end: var(--vkui_internal--FloatElement_vertical_indent);
inset-inline-end: var(--vkui_internal--FloatElement_horizontal_indent);
}

.placementMiddleStart {
inset-inline-start: var(--vkui_internal--FloatElement_horizontal_indent);
inset-block-start: 50%;
transform: translateY(-50%);
}

.placementMiddle {
inset-inline-start: 50%;
inset-block-start: 50%;
transform: translate(-50%, -50%);
}

.placementMiddleEnd {
inset-inline-end: var(--vkui_internal--FloatElement_horizontal_indent);
inset-block-start: 50%;
transform: translateY(-50%);
}
Loading

0 comments on commit 2789dd1

Please sign in to comment.