diff --git a/.storybook/stories/documentation/Blocks.mdx b/.storybook/stories/documentation/Blocks.mdx index b70ea7b28..f9b4c4a39 100644 --- a/.storybook/stories/documentation/Blocks.mdx +++ b/.storybook/stories/documentation/Blocks.mdx @@ -56,3 +56,5 @@ _[Common field types](?id=documentation-types&viewMode=docs)_ ## [Tabs](?path=/story/blocks-tabs--docs&viewMode=docs) ## [Form](?path=/story/blocks-form--docs&viewMode=docs) + +## [MarqueeLinks](?path=/story/blocks-marqueelinks--docs&viewMode=docs) diff --git a/package-lock.json b/package-lock.json index df7af68c3..79cc53869 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "js-yaml-source-map": "^0.2.2", "lodash": "^4.17.21", "monaco-editor": "^0.38.0", + "react-fast-marquee": "^1.6.5", "react-final-form": "^6.5.9", "react-monaco-editor": "^0.53.0", "react-player": "^2.9.0", @@ -24832,6 +24833,15 @@ "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz", "integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==" }, + "node_modules/react-fast-marquee": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/react-fast-marquee/-/react-fast-marquee-1.6.5.tgz", + "integrity": "sha512-swDnPqrT2XISAih0o74zQVE2wQJFMvkx+9VZXYYNSLb/CUcAzU9pNj637Ar2+hyRw6b4tP6xh4GQZip2ZCpQpg==", + "peerDependencies": { + "react": ">= 16.8.0 || ^18.0.0", + "react-dom": ">= 16.8.0 || ^18.0.0" + } + }, "node_modules/react-final-form": { "version": "6.5.9", "resolved": "https://registry.npmjs.org/react-final-form/-/react-final-form-6.5.9.tgz", diff --git a/package.json b/package.json index 14e0be484..5f028beb2 100644 --- a/package.json +++ b/package.json @@ -102,6 +102,7 @@ "js-yaml-source-map": "^0.2.2", "lodash": "^4.17.21", "monaco-editor": "^0.38.0", + "react-fast-marquee": "^1.6.5", "react-final-form": "^6.5.9", "react-monaco-editor": "^0.53.0", "react-player": "^2.9.0", diff --git a/src/blocks/MarqueeLinks/MarqueeLinks.scss b/src/blocks/MarqueeLinks/MarqueeLinks.scss new file mode 100644 index 000000000..2a94a1f3f --- /dev/null +++ b/src/blocks/MarqueeLinks/MarqueeLinks.scss @@ -0,0 +1,35 @@ +@import '../../../styles/mixins'; + +$block: '.#{$ns}marquee-links-block'; + +#{$block} { + &_left { + text-align: left; + } + + &_right { + text-align: right; + } + + &_center { + text-align: center; + } + + &__items { + .rfm-child img { + margin: 0 57px; + } + .rfm-overlay::after, + .rfm-overlay::before { + --gradient-color: var(--g-color-base-background); + } + } + + &__header { + margin-bottom: $indentXS; + } + + &__description { + margin-bottom: $indentSM; + } +} diff --git a/src/blocks/MarqueeLinks/MarqueeLinks.tsx b/src/blocks/MarqueeLinks/MarqueeLinks.tsx new file mode 100644 index 000000000..680a5590d --- /dev/null +++ b/src/blocks/MarqueeLinks/MarqueeLinks.tsx @@ -0,0 +1,56 @@ +import React, {useCallback} from 'react'; + +import {Link, Text} from '@gravity-ui/uikit'; +import Marquee from 'react-fast-marquee'; + +import {HTML, Image} from '../../components'; +import {MarqueeLinksBlockProps, MarqueeLinksItem} from '../../models'; +import {block} from '../../utils'; + +import './MarqueeLinks.scss'; + +const b = block('marquee-links-block'); + +export const MarqueeLinksBlock = ({ + title, + description, + textAlign = 'left', + speed = 10, + items, +}: MarqueeLinksBlockProps) => { + const renderItem = useCallback((item: MarqueeLinksItem) => { + const imageComponent = ; + if (item.url) { + return ( + + {imageComponent} + + ); + } + return imageComponent; + }, []); + + if (!items.length) return null; + + return ( +
+ {title && ( +
+ {title} +
+ )} + {description && ( +
+ + {description} + +
+ )} + + {items.map(renderItem)} + +
+ ); +}; + +export default MarqueeLinksBlock; diff --git a/src/blocks/MarqueeLinks/__stories__/MarqueeLinks.mdx b/src/blocks/MarqueeLinks/__stories__/MarqueeLinks.mdx new file mode 100644 index 000000000..1276a68ba --- /dev/null +++ b/src/blocks/MarqueeLinks/__stories__/MarqueeLinks.mdx @@ -0,0 +1,26 @@ +import {Meta} from '@storybook/blocks'; + +import {StoryTemplate} from '../../../demo/StoryTemplate.mdx'; +import * as MarqueeLinksStories from './MarqueeLinks.stories.tsx'; + + + +## Parameters + +`type: 'marquee-links-block'` + +`title?: string` + +`description?: string` + +`textAlign?: 'left' | 'right' | 'center'` + +`speed?: number` — default value 10. + +`items:` — Items, the block displays. + +- `src: string` - Link image. + +- `url: string` - Link + + diff --git a/src/blocks/MarqueeLinks/__stories__/MarqueeLinks.stories.tsx b/src/blocks/MarqueeLinks/__stories__/MarqueeLinks.stories.tsx new file mode 100644 index 000000000..1b1ef8fe4 --- /dev/null +++ b/src/blocks/MarqueeLinks/__stories__/MarqueeLinks.stories.tsx @@ -0,0 +1,22 @@ +import React from 'react'; + +import {Meta, StoryFn} from '@storybook/react'; + +import {PageConstructor} from '../../../containers/PageConstructor'; +import {MarqueeLinksBlockModel, MarqueeLinksBlockProps} from '../../../models'; +import MarqueeLinksBlock from '../MarqueeLinks'; + +import data from './data.json'; + +export default { + title: 'Blocks/MarqueeLinks', + component: MarqueeLinksBlock, +} as Meta; + +const DefaultTemplate: StoryFn = (args) => ( + +); + +export const Default = DefaultTemplate.bind({}); + +Default.args = data.default.content as MarqueeLinksBlockProps; diff --git a/src/blocks/MarqueeLinks/__stories__/data.json b/src/blocks/MarqueeLinks/__stories__/data.json new file mode 100644 index 000000000..159a80f1d --- /dev/null +++ b/src/blocks/MarqueeLinks/__stories__/data.json @@ -0,0 +1,25 @@ +{ + "default": { + "content": { + "type": "marquee-links-block", + "title": "Title", + "textAlign": "left", + "description": "description with a link", + "speed": 10, + "items": [ + { + "src": "/story-assets/icons-link_1_64.svg", + "url": "https://example.com" + }, + { + "src": "/story-assets/icons-link_1_64.svg", + "url": "https://example.com" + }, + { + "src": "/story-assets/icons-link_1_64.svg", + "url": "https://example.com" + } + ] + } + } +} diff --git a/src/blocks/MarqueeLinks/__tests__/MarqueeLinks.visual.test.tsx b/src/blocks/MarqueeLinks/__tests__/MarqueeLinks.visual.test.tsx new file mode 100644 index 000000000..d78c3d3d7 --- /dev/null +++ b/src/blocks/MarqueeLinks/__tests__/MarqueeLinks.visual.test.tsx @@ -0,0 +1,13 @@ +import React from 'react'; + +import {test} from '../../../../playwright/core/index'; + +import {Default} from './helpers'; + +test.describe('MarqueeLinks', () => { + test('render stories ', async ({mount, expectScreenshot, defaultDelay}) => { + await mount(); + await defaultDelay(); + await expectScreenshot({skipTheme: 'dark'}); + }); +}); diff --git a/src/blocks/MarqueeLinks/__tests__/helpers.tsx b/src/blocks/MarqueeLinks/__tests__/helpers.tsx new file mode 100644 index 000000000..b8edf0961 --- /dev/null +++ b/src/blocks/MarqueeLinks/__tests__/helpers.tsx @@ -0,0 +1,5 @@ +import {composeStories} from '@storybook/react'; + +import * as MarqueeLinksStories from '../__stories__/MarqueeLinks.stories'; + +export const {Default} = composeStories(MarqueeLinksStories); diff --git a/src/blocks/MarqueeLinks/schema.ts b/src/blocks/MarqueeLinks/schema.ts new file mode 100644 index 000000000..ab7b44f9a --- /dev/null +++ b/src/blocks/MarqueeLinks/schema.ts @@ -0,0 +1,42 @@ +import {BaseProps} from '../../schema/validators/common'; + +export const MarqueeLink = { + type: 'object', + additionalProperties: false, + required: ['src'], + properties: { + src: { + type: 'string', + }, + url: { + type: 'string', + }, + }, +}; + +export const MarqueeLinks = { + 'marquee-links-block': { + additionalProperties: false, + required: ['items'], + properties: { + ...BaseProps, + title: { + type: 'string', + }, + description: { + type: 'string', + }, + textAlign: { + type: 'string', + enum: ['left', 'right', 'center'], + }, + speed: { + type: 'number', + }, + items: { + type: 'array', + items: MarqueeLink, + }, + }, + }, +}; diff --git a/src/blocks/index.ts b/src/blocks/index.ts index 6ce472095..1d4c927a6 100644 --- a/src/blocks/index.ts +++ b/src/blocks/index.ts @@ -17,3 +17,4 @@ export {default as ContentLayoutBlock} from './ContentLayout/ContentLayout'; export {default as ShareBlock} from './Share/Share'; export {default as FilterBlock} from './FilterBlock/FilterBlock'; export {default as FormBlock} from './Form/Form'; +export {default as MarqueeLinks} from './MarqueeLinks/MarqueeLinks'; diff --git a/src/blocks/validators.ts b/src/blocks/validators.ts index a1a536817..8a0145895 100644 --- a/src/blocks/validators.ts +++ b/src/blocks/validators.ts @@ -13,3 +13,4 @@ export * from './Questions/schema'; export * from './Slider/schema'; export * from './Table/schema'; export * from './Share/schema'; +export * from './MarqueeLinks/schema'; diff --git a/src/constructor-items.ts b/src/constructor-items.ts index 3bb5c4a7d..aa56a49b7 100644 --- a/src/constructor-items.ts +++ b/src/constructor-items.ts @@ -11,6 +11,7 @@ import { IconsBlock, InfoBlock, MapBlock, + MarqueeLinks, MediaBlock, PromoFeaturesBlock, QuestionsBlock, @@ -62,6 +63,7 @@ export const blockMap = { [BlockType.MapBlock]: MapBlock, [BlockType.FilterBlock]: FilterBlock, [BlockType.FormBlock]: FormBlock, + [BlockType.MarqueeLinks]: MarqueeLinks, // unstable [BlockType.SliderNewBlock]: SliderNewBlock, }; diff --git a/src/models/constructor-items/blocks.ts b/src/models/constructor-items/blocks.ts index 715d0f235..1bc3fa23f 100644 --- a/src/models/constructor-items/blocks.ts +++ b/src/models/constructor-items/blocks.ts @@ -61,6 +61,7 @@ export enum BlockType { MapBlock = 'map-block', FilterBlock = 'filter-block', FormBlock = 'form-block', + MarqueeLinks = 'marquee-links-block', // unstable SliderNewBlock = 'slider-new-block', } @@ -280,6 +281,19 @@ export interface InfoBlockProps { rightContent?: Omit; } +export type MarqueeLinksItem = { + src: string; + url?: string; +}; + +export interface MarqueeLinksBlockProps { + title?: string; + description?: string; + textAlign?: 'left' | 'right' | 'center'; + speed?: number; + items: MarqueeLinksItem[]; +} + export interface TableProps { content: string[][]; legend?: string[]; @@ -529,6 +543,10 @@ export type FormBlockModel = { type: BlockType.FormBlock; } & FormBlockProps; +export type MarqueeLinksBlockModel = { + type: BlockType.MarqueeLinks; +} & MarqueeLinksBlockProps; + // unstable block models export type SliderNewBlockModel = { type: BlockType.SliderNewBlock; @@ -553,7 +571,8 @@ type BlockModels = | ContentLayoutBlockModel | ShareBLockModel | FilterBlockModel - | FormBlockModel; + | FormBlockModel + | MarqueeLinksBlockModel; type UnstableBlockModels = SliderNewBlockModel; diff --git a/src/schema/constants.ts b/src/schema/constants.ts index 63d4b62a5..1278d9225 100644 --- a/src/schema/constants.ts +++ b/src/schema/constants.ts @@ -14,6 +14,7 @@ import { IconsBlock, InfoBlock, MapBlock, + MarqueeLinks, MediaBlock, PromoFeaturesBlock, QuestionsBlock, @@ -56,6 +57,7 @@ export const blockSchemas: Record = { ...FilterBlock, ...FormBlock, ...SliderNewBlock, + ...MarqueeLinks, }; export const cardSchemas = { @@ -86,6 +88,7 @@ export const constructorBlockSchemaNames = [ 'info-block', 'table-block', 'tabs-block', + 'marquee-links-block', /** @deprecated */ 'price-detailed', 'header-slider-block', diff --git a/src/schema/validators/blocks.ts b/src/schema/validators/blocks.ts index ca30f4929..684547b10 100644 --- a/src/schema/validators/blocks.ts +++ b/src/schema/validators/blocks.ts @@ -18,3 +18,4 @@ export * from '../../blocks/Share/schema'; export * from '../../blocks/FilterBlock/schema'; export * from '../../blocks/Form/schema'; export * from '../../blocks/SliderNew/schema'; +export * from '../../blocks/MarqueeLinks/schema';