From 7e3dc0b68ac046f29890608a8519ba3ee0d43a0f Mon Sep 17 00:00:00 2001 From: Roman Karpov Date: Fri, 1 Mar 2024 09:34:31 +0100 Subject: [PATCH] feat(SubBlocks): add possibility to position controls within cards at footer (#823) * feat(SubBlocks): add possibility to position controls within cards at footer * fix: comments * feat: add storybook for changes * fix: apply the required behavioral changes * fix: resolve comments * fix: lint --- src/components/Buttons/Buttons.scss | 19 +++++ src/components/Buttons/Buttons.tsx | 49 +++++++++++++ src/components/CardBase/CardBase.tsx | 22 +++--- src/components/Link/Links.tsx | 27 ------- src/components/Links/Links.scss | 23 ++++++ src/components/Links/Links.tsx | 49 +++++++++++++ src/components/index.ts | 3 +- src/models/constructor-items/common.ts | 5 ++ src/models/constructor-items/sub-blocks.ts | 5 +- .../BackgroundCard/BackgroundCard.tsx | 30 +++++++- .../__stories__/BackgroundCard.stories.tsx | 58 +++++++++++++++ .../BackgroundCard/__stories__/data.json | 55 ++++++++++++++ src/sub-blocks/BackgroundCard/schema.ts | 4 ++ src/sub-blocks/BasicCard/BasicCard.tsx | 27 +++++-- .../__stories__/BasicCard.stories.tsx | 52 ++++++++++++++ .../BasicCard/__stories__/data.json | 57 +++++++++++++++ src/sub-blocks/BasicCard/schema.ts | 4 ++ src/sub-blocks/Content/Content.scss | 26 ------- src/sub-blocks/Content/Content.tsx | 72 +++++-------------- src/sub-blocks/LayoutItem/LayoutItem.scss | 5 ++ src/sub-blocks/LayoutItem/LayoutItem.tsx | 27 +++++-- .../__stories__/LayoutItem.stories.tsx | 46 ++++++++++++ .../LayoutItem/__stories__/data.json | 69 ++++++++++++++++++ src/sub-blocks/LayoutItem/schema.ts | 4 ++ src/sub-blocks/PriceCard/PriceCard.scss | 10 +-- src/sub-blocks/PriceCard/PriceCard.tsx | 32 ++------- .../__stories__/PriceCard.stories.tsx | 31 ++++++-- src/sub-blocks/PriceCard/schema.ts | 4 ++ .../CardFooterControlsContainer.scss | 7 ++ .../renderCardFooterControlsContainer.tsx | 12 ++++ .../ContentControls.scss | 17 +++++ .../renderContentControls.tsx | 48 +++++++++++++ 32 files changed, 725 insertions(+), 174 deletions(-) create mode 100644 src/components/Buttons/Buttons.scss create mode 100644 src/components/Buttons/Buttons.tsx delete mode 100644 src/components/Link/Links.tsx create mode 100644 src/components/Links/Links.scss create mode 100644 src/components/Links/Links.tsx create mode 100644 src/sub-blocks/renderCardFooterControlsContainer/CardFooterControlsContainer.scss create mode 100644 src/sub-blocks/renderCardFooterControlsContainer/renderCardFooterControlsContainer.tsx create mode 100644 src/utils/renderContentControls/ContentControls.scss create mode 100644 src/utils/renderContentControls/renderContentControls.tsx diff --git a/src/components/Buttons/Buttons.scss b/src/components/Buttons/Buttons.scss new file mode 100644 index 000000000..a80fe68fd --- /dev/null +++ b/src/components/Buttons/Buttons.scss @@ -0,0 +1,19 @@ +@import '../../../styles/variables.scss'; + +$block: '.#{$ns}buttons'; + +#{$block} { + display: flex; + flex-wrap: wrap; + column-gap: $indentXXS; + + &_size { + &_s { + row-gap: $indentXXXS; + } + + &_l { + row-gap: $indentXXS; + } + } +} diff --git a/src/components/Buttons/Buttons.tsx b/src/components/Buttons/Buttons.tsx new file mode 100644 index 000000000..04485c727 --- /dev/null +++ b/src/components/Buttons/Buttons.tsx @@ -0,0 +1,49 @@ +import React from 'react'; + +import {ButtonProps, ContentSize} from '../../models'; +import {block} from '../../utils'; +import Button from '../Button/Button'; + +import './Buttons.scss'; + +const b = block('buttons'); + +type ButtonsProps = { + className?: string; + buttons?: ButtonProps[]; + size?: ContentSize; + titleId?: string; + qa?: string; + buttonQa?: string; +}; + +function getButtonSize(size: ContentSize) { + switch (size) { + case 's': + return 'm'; + case 'l': + default: + return 'xl'; + } +} + +const Buttons: React.FC = ({className, titleId, buttons, size = 's', qa, buttonQa}) => + buttons ? ( +
+ {buttons.map((item) => ( +
+ ) : null; + +export default Buttons; diff --git a/src/components/CardBase/CardBase.tsx b/src/components/CardBase/CardBase.tsx index a6ec22b31..6d3c79f6d 100644 --- a/src/components/CardBase/CardBase.tsx +++ b/src/components/CardBase/CardBase.tsx @@ -1,4 +1,11 @@ -import React, {Children, Fragment, HTMLAttributeAnchorTarget, ReactElement} from 'react'; +import React, { + Children, + Fragment, + HTMLAttributeAnchorTarget, + PropsWithChildren, + ReactElement, + isValidElement, +} from 'react'; import {Link} from '@gravity-ui/uikit'; @@ -19,11 +26,10 @@ import RouterLink from '../RouterLink/RouterLink'; import './CardBase.scss'; -export interface CardBaseProps extends AnalyticsEventsBase, CardBaseParams { +export interface CardBaseProps extends AnalyticsEventsBase, CardBaseParams, PropsWithChildren { className?: string; bodyClassName?: string; contentClassName?: string; - children: ReactElement | ReactElement[]; url?: string; urlTitle?: string; target?: HTMLAttributeAnchorTarget; @@ -86,11 +92,11 @@ export const Layout = (props: CardBaseProps) => { } } - if (Children.count(children) === 1) { - handleChild(children as ReactElement); - } else { - Children.forEach(children, handleChild); - } + Children.toArray(children).forEach((child) => { + if (isValidElement(child)) { + handleChild(child); + } + }); const cardContent = ( diff --git a/src/components/Link/Links.tsx b/src/components/Link/Links.tsx deleted file mode 100644 index f23b13321..000000000 --- a/src/components/Link/Links.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import React, {Fragment} from 'react'; - -import {ClassNameProps, LinkProps} from '../../models'; - -import Link from './Link'; - -interface LinksProps extends ClassNameProps { - links?: LinkProps[]; -} - -const Links = (props: LinksProps) => { - const {links, className} = props; - - if (!links) { - return null; - } - - return ( - - {links.map((link: LinkProps) => ( - - ))} - - ); -}; - -export default Links; diff --git a/src/components/Links/Links.scss b/src/components/Links/Links.scss new file mode 100644 index 000000000..766f85447 --- /dev/null +++ b/src/components/Links/Links.scss @@ -0,0 +1,23 @@ +@import '../../../styles/variables.scss'; + +$block: '.#{$ns}links'; + +#{$block} { + display: flex; + flex-direction: column; + align-items: baseline; + + &__link { + margin-top: 0px; + } + + &_size { + &_s { + gap: $indentXXXS; + } + + &_l { + gap: $indentXXS; + } + } +} diff --git a/src/components/Links/Links.tsx b/src/components/Links/Links.tsx new file mode 100644 index 000000000..0cededb96 --- /dev/null +++ b/src/components/Links/Links.tsx @@ -0,0 +1,49 @@ +import React from 'react'; + +import {ContentSize, LinkProps} from '../../models'; +import {block} from '../../utils'; +import Link from '../Link/Link'; + +import './Links.scss'; + +const b = block('links'); + +function getLinkSize(size: ContentSize) { + switch (size) { + case 's': + return 'm'; + case 'l': + default: + return 'l'; + } +} + +type LinksProps = { + className?: string; + titleId?: string; + links?: LinkProps[]; + size?: ContentSize; + qa?: string; + linkQa?: string; +}; + +const Links: React.FC = ({className, titleId, links, size = 's', qa, linkQa}) => + links ? ( +
+ {links?.map((link) => ( + + ))} +
+ ) : null; + +export default Links; diff --git a/src/components/index.ts b/src/components/index.ts index 9335070d7..ee638c4cf 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -6,6 +6,7 @@ export {default as BackLink} from './BackLink/BackLink'; export {default as BalancedMasonry} from './BalancedMasonry/BalancedMasonry'; export {default as BlockBase} from './BlockBase/BlockBase'; export {default as Button} from './Button/Button'; +export {default as Buttons} from './Buttons/Buttons'; export {default as CardBase} from './CardBase/CardBase'; export {default as ErrorWrapper} from './ErrorWrapper/ErrorWrapper'; export {default as FileLink} from './FileLink/FileLink'; @@ -16,7 +17,7 @@ export {default as HeaderBreadcrumbs} from './HeaderBreadcrumbs/HeaderBreadcrumb export {default as Image} from './Image/Image'; export {default as ImageBase} from './ImageBase/ImageBase'; export {default as Link} from './Link/Link'; -export {default as Links} from './Link/Links'; +export {default as Links} from './Links/Links'; export {default as Media} from './Media/Media'; export {default as OutsideClick} from './OutsideClick/OutsideClick'; export {default as ReactPlayer} from './ReactPlayer/ReactPlayer'; diff --git a/src/models/constructor-items/common.ts b/src/models/constructor-items/common.ts index 7ffe69821..a1d830318 100644 --- a/src/models/constructor-items/common.ts +++ b/src/models/constructor-items/common.ts @@ -392,11 +392,16 @@ export type MediaView = 'fit' | 'full'; // card export type MediaBorder = 'shadow' | 'line' | 'none'; export type CardBorder = MediaBorder; +export type ControlPosition = 'content' | 'footer'; export interface CardBaseProps { border?: CardBorder; } +export type CardLayoutProps = { + controlPosition?: ControlPosition; +}; + //price export interface PriceDescriptionProps { title: string; diff --git a/src/models/constructor-items/sub-blocks.ts b/src/models/constructor-items/sub-blocks.ts index 89cbb1f5a..7673c8710 100644 --- a/src/models/constructor-items/sub-blocks.ts +++ b/src/models/constructor-items/sub-blocks.ts @@ -9,6 +9,7 @@ import { ButtonPixel, ButtonProps, CardBaseProps, + CardLayoutProps, ContentTheme, DividerSize, ImageCardMargins, @@ -134,6 +135,7 @@ export interface QuoteProps extends Themable, CardBaseProps { export interface BackgroundCardProps extends CardBaseProps, AnalyticsEventsBase, + CardLayoutProps, Omit { url?: string; urlTitle?: string; @@ -145,6 +147,7 @@ export interface BackgroundCardProps export interface BasicCardProps extends CardBaseProps, AnalyticsEventsBase, + CardLayoutProps, Omit { url: string; urlTitle?: string; @@ -178,7 +181,7 @@ export interface PriceCardProps extends CardBaseProps, Pick; media?: MediaProps; metaInfo?: string[]; diff --git a/src/sub-blocks/BackgroundCard/BackgroundCard.tsx b/src/sub-blocks/BackgroundCard/BackgroundCard.tsx index 90c644076..66dc63467 100644 --- a/src/sub-blocks/BackgroundCard/BackgroundCard.tsx +++ b/src/sub-blocks/BackgroundCard/BackgroundCard.tsx @@ -1,10 +1,14 @@ -import React from 'react'; +import React, {useMemo} from 'react'; + +import {useUniqId} from '@gravity-ui/uikit'; import {BackgroundImage, CardBase} from '../../components/'; import {useTheme} from '../../context/theme'; import {BackgroundCardProps} from '../../models'; import {block, getThemedValue} from '../../utils'; +import renderContentControls from '../../utils/renderContentControls/renderContentControls'; import Content from '../Content/Content'; +import renderCardFooterControlsContainer from '../renderCardFooterControlsContainer/renderCardFooterControlsContainer'; import './BackgroundCard.scss'; @@ -25,11 +29,29 @@ const BackgroundCard = (props: BackgroundCardProps) => { buttons, analyticsEvents, urlTitle, + controlPosition = 'content', } = props; + const titleId = useUniqId(); + const theme = useTheme(); const hasBackgroundColor = backgroundColor || cardTheme !== 'default'; const borderType = hasBackgroundColor ? 'none' : border; + const areControlsInFooter = !paddingBottom && controlPosition === 'footer'; + + const footerControls = useMemo( + () => + renderContentControls( + { + links: areControlsInFooter ? links : undefined, + buttons: areControlsInFooter ? buttons : undefined, + size: 's', + titleId, + }, + renderCardFooterControlsContainer, + ), + [areControlsInFooter, links, buttons, titleId], + ); return ( { style={{backgroundColor}} /> + {footerControls} ); }; diff --git a/src/sub-blocks/BackgroundCard/__stories__/BackgroundCard.stories.tsx b/src/sub-blocks/BackgroundCard/__stories__/BackgroundCard.stories.tsx index 50ca5f9e8..282638255 100644 --- a/src/sub-blocks/BackgroundCard/__stories__/BackgroundCard.stories.tsx +++ b/src/sub-blocks/BackgroundCard/__stories__/BackgroundCard.stories.tsx @@ -3,6 +3,10 @@ import React from 'react'; import yfm from '@doc-tools/transform'; import {Meta, StoryFn} from '@storybook/react'; +import CardLayout from '../../../blocks/CardLayout/CardLayout'; +import {BlockBase} from '../../../components'; +import {ConstructorRow} from '../../../containers/PageConstructor/components/ConstructorRow'; +import {Grid} from '../../../grid'; import {BackgroundCardProps, ButtonProps, LinkProps} from '../../../models'; import BackgroundCard from '../BackgroundCard'; @@ -18,6 +22,10 @@ export default { backgroundColor: { control: {type: 'color'}, }, + paddingBottom: { + control: {type: 'radio', labels: {undefined: 'default'}}, + options: [undefined, 's', 'm', 'l', 'xl'], + }, }, } as Meta; @@ -95,6 +103,41 @@ const WithUrlTemplate: StoryFn<{items: BackgroundCardProps[]}> = (args) => ( ); +const ControlPositionTemplate: StoryFn = (args) => ( + + + + + {data.cardLayout.items.map((item, index) => ( + )} + {...args} + controlPosition="content" + /> + ))} + + + + + {data.cardLayout.items.map((item, index) => ( + )} + {...args} + controlPosition="footer" + /> + ))} + + + + +); + export const Default = DefaultTemplate.bind({}); export const WithBackgroundImage = DefaultTemplate.bind({}); export const Paddings = PaddingsTemplate.bind({}); @@ -102,6 +145,7 @@ export const CardThemes = CardThemesTemplate.bind([]); export const BorderLine = DefaultTemplate.bind({}); export const BackgroundColor = BackgroundColorTemplate.bind({}); export const WithUrl = WithUrlTemplate.bind({}); +export const ControlPosition = ControlPositionTemplate.bind({}); const DefaultArgs = { title: data.common.title, @@ -154,3 +198,17 @@ WithUrl.args = { urlTitle: data.urlTitle, })) as BackgroundCardProps[], }; + +ControlPosition.argTypes = { + controlPosition: {table: {disable: true}}, + url: {table: {disable: true}}, + urlTitle: {table: {disable: true}}, + analyticsEvents: {table: {disable: true}}, + title: {table: {disable: true}}, + text: {table: {disable: true}}, + titleId: {table: {disable: true}}, + textId: {table: {disable: true}}, + list: {table: {disable: true}}, + links: {table: {disable: true}}, + buttons: {table: {disable: true}}, +}; diff --git a/src/sub-blocks/BackgroundCard/__stories__/data.json b/src/sub-blocks/BackgroundCard/__stories__/data.json index 22bf5adb1..ec729e6a1 100644 --- a/src/sub-blocks/BackgroundCard/__stories__/data.json +++ b/src/sub-blocks/BackgroundCard/__stories__/data.json @@ -117,5 +117,60 @@ ] } ] + }, + "cardLayout": { + "contentTitle": "With controlPosition = content", + "footerTitle": "With controlPosition = footer", + "footerDescription": "Please note that the controlPosition prop manages the position of buttons and links only when paddingBottom = default.", + "items": [ + { + "title": "Lorem ipsumt", + "text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. In egestas erat imperdiet sed euismod nisi porta lorem. Amet purus gravida quis blandit turpis cursus in hac.", + "buttons": [ + { + "text": "Button 1", + "theme": "action", + "url": "#test1" + }, + { + "text": "Button 2", + "theme": "outlined", + "url": "#test2" + } + ] + }, + { + "title": "Lorem ipsumt", + "text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. In egestas erat imperdiet sed euismod nisi porta lorem. Amet purus gravida quis blandit turpis cursus in hac. Pulvinar sapien et ligula ullamcorper malesuada proin libero nunc consequat.", + "buttons": [ + { + "text": "Button 1", + "theme": "action", + "url": "#test1" + }, + { + "text": "Button 2", + "theme": "outlined", + "url": "#test2" + } + ] + }, + { + "title": "Lorem ipsumt", + "text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. In egestas erat imperdiet sed euismod nisi porta lorem.", + "buttons": [ + { + "text": "Button 1", + "theme": "action", + "url": "#test1" + }, + { + "text": "Button 2", + "theme": "outlined", + "url": "#test2" + } + ] + } + ] } } diff --git a/src/sub-blocks/BackgroundCard/schema.ts b/src/sub-blocks/BackgroundCard/schema.ts index c2caa8470..22e4d8e76 100644 --- a/src/sub-blocks/BackgroundCard/schema.ts +++ b/src/sub-blocks/BackgroundCard/schema.ts @@ -42,6 +42,10 @@ export const BackgroundCard = { }, ], }, + controlPosition: { + type: 'string', + enum: ['content', 'footer'], + }, }, }, }; diff --git a/src/sub-blocks/BasicCard/BasicCard.tsx b/src/sub-blocks/BasicCard/BasicCard.tsx index 4969c6f82..342adbcdd 100644 --- a/src/sub-blocks/BasicCard/BasicCard.tsx +++ b/src/sub-blocks/BasicCard/BasicCard.tsx @@ -1,13 +1,14 @@ -import React from 'react'; +import React, {useMemo} from 'react'; import {useUniqId} from '@gravity-ui/uikit'; -import CardBase from '../../components/CardBase/CardBase'; -import IconWrapper from '../../components/IconWrapper/IconWrapper'; +import {CardBase, IconWrapper} from '../../components'; import {BasicCardProps} from '../../models'; import {IconPosition} from '../../models/constructor-items/sub-blocks'; import {block} from '../../utils'; +import renderContentControls from '../../utils/renderContentControls/renderContentControls'; import Content from '../Content/Content'; +import renderCardFooterControlsContainer from '../renderCardFooterControlsContainer/renderCardFooterControlsContainer'; import './BasicCard.scss'; @@ -23,10 +24,25 @@ const BasicCard = (props: BasicCardProps) => { list, buttons, iconPosition = IconPosition.Top, + controlPosition = 'content', ...cardParams } = props; const titleId = useUniqId(); const descriptionId = useUniqId(); + const areControlsInFooter = controlPosition === 'footer'; + const footerControls = useMemo( + () => + renderContentControls( + { + links: areControlsInFooter ? links : undefined, + buttons: areControlsInFooter ? buttons : undefined, + size: 's', + titleId, + }, + renderCardFooterControlsContainer, + ), + [areControlsInFooter, links, buttons, titleId], + ); return ( { text={text} textId={descriptionId} additionalInfo={additionalInfo} - links={links} + links={areControlsInFooter ? undefined : links} list={list} - buttons={buttons} + buttons={areControlsInFooter ? undefined : buttons} size="s" colSizes={{all: 12, md: 12}} /> + {footerControls} ); }; diff --git a/src/sub-blocks/BasicCard/__stories__/BasicCard.stories.tsx b/src/sub-blocks/BasicCard/__stories__/BasicCard.stories.tsx index 4fed42732..a384f52af 100644 --- a/src/sub-blocks/BasicCard/__stories__/BasicCard.stories.tsx +++ b/src/sub-blocks/BasicCard/__stories__/BasicCard.stories.tsx @@ -4,6 +4,10 @@ import yfm from '@doc-tools/transform'; import {Meta, StoryFn} from '@storybook/react'; import {yfmTransform} from '../../../../.storybook/utils'; +import CardLayout from '../../../blocks/CardLayout/CardLayout'; +import {BlockBase} from '../../../components'; +import {ConstructorRow} from '../../../containers/PageConstructor/components/ConstructorRow'; +import {Grid} from '../../../grid'; import {BasicCardProps, ContentItemProps} from '../../../models'; import {IconPosition} from '../../../models/constructor-items/sub-blocks'; import BasicCard from '../BasicCard'; @@ -110,11 +114,43 @@ const WithUrlTemplate: StoryFn = (args) => ( ); +const ControlPositionTemplate: StoryFn = (args) => ( + + + + + {data.cardLayout.items.map((item, index) => ( + )} + {...args} + controlPosition="content" + /> + ))} + + + + + {data.cardLayout.items.map((item, index) => ( + )} + {...args} + controlPosition="footer" + /> + ))} + + + + +); + export const Default = DefaultTemplate.bind({}); export const WithIcon = WithIconTemplate.bind({}); export const WithBorder = WithBorderTemplate.bind({}); export const WithUrl = WithUrlTemplate.bind({}); export const WithContentList = WithContentListTemplate.bind({}); +export const ControlPosition = ControlPositionTemplate.bind({}); const DefaultArgs = { ...data.default.content, @@ -135,3 +171,19 @@ WithUrl.args = { WithContentList.args = { ...DefaultArgs, } as BasicCardProps; + +ControlPosition.argTypes = { + controlPosition: {table: {disable: true}}, + url: {table: {disable: true}}, + urlTitle: {table: {disable: true}}, + analyticsEvents: {table: {disable: true}}, + title: {table: {disable: true}}, + text: {table: {disable: true}}, + titleId: {table: {disable: true}}, + textId: {table: {disable: true}}, + icon: {table: {disable: true}}, + list: {table: {disable: true}}, + links: {table: {disable: true}}, + buttons: {table: {disable: true}}, + target: {table: {disable: true}}, +}; diff --git a/src/sub-blocks/BasicCard/__stories__/data.json b/src/sub-blocks/BasicCard/__stories__/data.json index 18bd3f5e8..699a83fed 100644 --- a/src/sub-blocks/BasicCard/__stories__/data.json +++ b/src/sub-blocks/BasicCard/__stories__/data.json @@ -70,5 +70,62 @@ "text": "**Ut enim ad minim veniam** [quis nostrud](https://example.com) exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.", "urlTitle": "Opens in a new tab" } + }, + "cardLayout": { + "contentTitle": "With controlPosition = content", + "footerTitle": "With controlPosition = footer", + "items": [ + { + "icon": "/story-assets/icon_1_light.svg", + "title": "Lorem ipsumt", + "text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. In egestas erat imperdiet sed euismod nisi porta lorem. Amet purus gravida quis blandit turpis cursus in hac.", + "buttons": [ + { + "text": "Button 1", + "theme": "action", + "url": "#test1" + }, + { + "text": "Button 2", + "theme": "outlined", + "url": "#test2" + } + ] + }, + { + "icon": "/story-assets/icon_2_light.svg", + "title": "Lorem ipsumt", + "text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. In egestas erat imperdiet sed euismod nisi porta lorem. Amet purus gravida quis blandit turpis cursus in hac. Pulvinar sapien et ligula ullamcorper malesuada proin libero nunc consequat.", + "buttons": [ + { + "text": "Button 1", + "theme": "action", + "url": "#test1" + }, + { + "text": "Button 2", + "theme": "outlined", + "url": "#test2" + } + ] + }, + { + "icon": "/story-assets/icon_3_light.svg", + "title": "Lorem ipsumt", + "text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. In egestas erat imperdiet sed euismod nisi porta lorem.", + "buttons": [ + { + "text": "Button 1", + "theme": "action", + "url": "#test1" + }, + { + "text": "Button 2", + "theme": "outlined", + "url": "#test2" + } + ] + } + ] } } diff --git a/src/sub-blocks/BasicCard/schema.ts b/src/sub-blocks/BasicCard/schema.ts index 19673004a..59142b03c 100644 --- a/src/sub-blocks/BasicCard/schema.ts +++ b/src/sub-blocks/BasicCard/schema.ts @@ -29,6 +29,10 @@ export const BasicCard = { type: 'string', enum: ['top', 'left'], }, + controlPosition: { + type: 'string', + enum: ['content', 'footer'], + }, }, }, }; diff --git a/src/sub-blocks/Content/Content.scss b/src/sub-blocks/Content/Content.scss index 8dca74385..29328aa7e 100644 --- a/src/sub-blocks/Content/Content.scss +++ b/src/sub-blocks/Content/Content.scss @@ -41,18 +41,6 @@ $darkSecondary: var(--g-color-text-dark-secondary); } } - &__buttons { - display: flex; - flex-wrap: wrap; - column-gap: $indentXXS; - } - - &__links { - display: flex; - flex-direction: column; - align-items: baseline; - } - &__link { display: block; } @@ -99,13 +87,6 @@ $darkSecondary: var(--g-color-text-dark-secondary); } } - #{$block}__links, - #{$block}__link, - #{$block}__buttons, - #{$block}__button { - margin-top: $indentXXXS; - } - #{$block}__list { margin-top: $indentXS; } @@ -129,13 +110,6 @@ $darkSecondary: var(--g-color-text-dark-secondary); @include text-size(body-3); } - #{$block}__links, - #{$block}__link, - #{$block}__buttons, - #{$block}__button { - margin-top: $indentXXS; - } - #{$block}__list { margin-top: $indentSM; } diff --git a/src/sub-blocks/Content/Content.tsx b/src/sub-blocks/Content/Content.tsx index e44bf3507..c49ff65c0 100644 --- a/src/sub-blocks/Content/Content.tsx +++ b/src/sub-blocks/Content/Content.tsx @@ -1,13 +1,14 @@ -import React from 'react'; +import React, {useMemo} from 'react'; import {useUniqId} from '@gravity-ui/uikit'; -import {Button, ContentList, Link as LinkBlock, Title, YFMWrapper} from '../../components'; +import {ContentList, Title, YFMWrapper} from '../../components'; import {Col} from '../../grid'; import {ClassNameProps, ContentBlockProps, ContentSize, TitleItemProps} from '../../models'; import {QAProps} from '../../models/common'; import {block} from '../../utils'; import {getQaAttrubutes} from '../../utils/blocks'; +import renderContentControls from '../../utils/renderContentControls/renderContentControls'; import './Content.scss'; @@ -23,26 +24,6 @@ function getTextSize(size: ContentSize) { } } -function getLinkSize(size: ContentSize) { - switch (size) { - case 's': - return 'm'; - case 'l': - default: - return 'l'; - } -} - -function getButtonSize(size: ContentSize) { - switch (size) { - case 's': - return 'm'; - case 'l': - default: - return 'xl'; - } -} - export type ContentProps = ContentBlockProps & ClassNameProps & QAProps; const Content = (props: ContentProps) => { @@ -73,6 +54,18 @@ const Content = (props: ContentProps) => { const defaultTitleId = useUniqId(); const titleId = titleIdFromProps || defaultTitleId; + const controls = useMemo( + () => + renderContentControls({ + size, + links, + buttons, + titleId, + qa: qaAttributes, + }), + [size, links, buttons, titleId, qaAttributes], + ); + return ( { /> )} - {links && ( -
- {links.map((link) => ( - - ))} -
- )} - {buttons && ( -
- {buttons.map((item) => ( -
- )} + {controls} ); }; diff --git a/src/sub-blocks/LayoutItem/LayoutItem.scss b/src/sub-blocks/LayoutItem/LayoutItem.scss index 18b7a4875..8370360b3 100644 --- a/src/sub-blocks/LayoutItem/LayoutItem.scss +++ b/src/sub-blocks/LayoutItem/LayoutItem.scss @@ -4,6 +4,10 @@ $block: '.#{$ns}layout-item'; #{$block} { + display: flex; + flex-direction: column; + height: 100%; + &__media { width: 100%; display: block; @@ -19,6 +23,7 @@ $block: '.#{$ns}layout-item'; } &__content { + flex: auto; margin: $indentXS $indentXS 0 0; &_no-media { diff --git a/src/sub-blocks/LayoutItem/LayoutItem.tsx b/src/sub-blocks/LayoutItem/LayoutItem.tsx index 8a2e92137..f26b054e5 100644 --- a/src/sub-blocks/LayoutItem/LayoutItem.tsx +++ b/src/sub-blocks/LayoutItem/LayoutItem.tsx @@ -1,8 +1,11 @@ -import React from 'react'; +import React, {useMemo} from 'react'; + +import {useUniqId} from '@gravity-ui/uikit'; import {FullscreenMedia, IconWrapper, Media, MetaInfo} from '../../components'; import {ContentBlockProps, LayoutItemProps} from '../../models'; import {block} from '../../utils'; +import renderContentControls from '../../utils/renderContentControls/renderContentControls'; import Content from '../Content/Content'; import {getLayoutItemLinks, hasFullscreen, showFullscreenIcon} from './utils'; @@ -12,7 +15,7 @@ import './LayoutItem.scss'; const b = block('layout-item'); const LayoutItem = ({ - content: {links, ...content}, + content: {links, buttons, ...content}, metaInfo, media, border, @@ -20,13 +23,28 @@ const LayoutItem = ({ icon, className, analyticsEvents, + controlPosition = 'content', }: LayoutItemProps) => { + const normalizedLinks = useMemo(() => getLayoutItemLinks(links), [links]); + const areControlsInFooter = controlPosition === 'footer'; + const contentProps: ContentBlockProps = { ...content, - links: getLayoutItemLinks(links), + ...(areControlsInFooter ? {} : {links: normalizedLinks, buttons}), size: 's', colSizes: {all: 12, md: 12}, }; + const titleId = useUniqId(); + const footerControls = useMemo( + () => + renderContentControls({ + links: areControlsInFooter ? links : undefined, + buttons: areControlsInFooter ? buttons : undefined, + size: 's', + titleId, + }), + [areControlsInFooter, links, buttons, titleId], + ); const renderMedia = () => { if (!media) { return null; @@ -56,9 +74,10 @@ const LayoutItem = ({ {metaInfo && }
- +
+ {footerControls} ); }; diff --git a/src/sub-blocks/LayoutItem/__stories__/LayoutItem.stories.tsx b/src/sub-blocks/LayoutItem/__stories__/LayoutItem.stories.tsx index 5b22fd966..716a4dfc9 100644 --- a/src/sub-blocks/LayoutItem/__stories__/LayoutItem.stories.tsx +++ b/src/sub-blocks/LayoutItem/__stories__/LayoutItem.stories.tsx @@ -3,6 +3,10 @@ import React from 'react'; import {Meta, StoryFn} from '@storybook/react'; import {yfmTransform} from '../../../../.storybook/utils'; +import CardLayout from '../../../blocks/CardLayout/CardLayout'; +import {BlockBase} from '../../../components'; +import {ConstructorRow} from '../../../containers/PageConstructor/components/ConstructorRow'; +import {Grid} from '../../../grid'; import {LayoutItemProps} from '../../../models'; import LayoutItem from '../LayoutItem'; @@ -32,11 +36,43 @@ const WithIconTemplate: StoryFn = (args) => ( ); +const ControlPositionTemplate: StoryFn = (args) => ( + + + + + {data.cardLayout.items.map((item, index) => ( + )} + {...args} + controlPosition="content" + /> + ))} + + + + + {data.cardLayout.items.map((item, index) => ( + )} + {...args} + controlPosition="footer" + /> + ))} + + + + +); + export const Default = DefaultTemplate.bind({}); export const Fullscreen = DefaultTemplate.bind({}); export const MetaInfo = DefaultTemplate.bind({}); export const Youtube = DefaultTemplate.bind({}); export const WithIcon = WithIconTemplate.bind({}); +export const ControlPosition = ControlPositionTemplate.bind({}); const DefaultArgs = { ...data.default.content, @@ -59,3 +95,13 @@ WithIcon.args = { media: undefined, icon: data.withIcon.iconTop as LayoutItemProps['icon'], }; + +ControlPosition.argTypes = { + content: {table: {disable: true}}, + media: {table: {disable: true}}, + metaInfo: {table: {disable: true}}, + icon: {table: {disable: true}}, + className: {table: {disable: true}}, + controlPosition: {table: {disable: true}}, + analyticsEvents: {table: {disable: true}}, +}; diff --git a/src/sub-blocks/LayoutItem/__stories__/data.json b/src/sub-blocks/LayoutItem/__stories__/data.json index 30ec30b90..12d575876 100644 --- a/src/sub-blocks/LayoutItem/__stories__/data.json +++ b/src/sub-blocks/LayoutItem/__stories__/data.json @@ -41,5 +41,74 @@ "value": "/story-assets/icon_2_light.svg", "position": "left" } + }, + "cardLayout": { + "contentTitle": "With controlPosition = content", + "footerTitle": "With controlPosition = footer", + "items": [ + { + "media": { + "image": "/story-assets/img-mini_4-12_light.png" + }, + "content": { + "title": "Lorem ipsumt", + "text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. In egestas erat imperdiet sed euismod nisi porta lorem. Amet purus gravida quis blandit turpis cursus in hac.", + "buttons": [ + { + "text": "Button 1", + "theme": "action", + "url": "#test1" + }, + { + "text": "Button 2", + "theme": "outlined", + "url": "#test2" + } + ] + } + }, + { + "media": { + "image": "/story-assets/img-mini_4-12_light.png" + }, + "content": { + "title": "Lorem ipsumt", + "text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. In egestas erat imperdiet sed euismod nisi porta lorem. Amet purus gravida quis blandit turpis cursus in hac. Pulvinar sapien et ligula ullamcorper malesuada proin libero nunc consequat.", + "buttons": [ + { + "text": "Button 1", + "theme": "action", + "url": "#test1" + }, + { + "text": "Button 2", + "theme": "outlined", + "url": "#test2" + } + ] + } + }, + { + "media": { + "image": "/story-assets/img-mini_4-12_light.png" + }, + "content": { + "title": "Lorem ipsumt", + "text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. In egestas erat imperdiet sed euismod nisi porta lorem.", + "buttons": [ + { + "text": "Button 1", + "theme": "action", + "url": "#test1" + }, + { + "text": "Button 2", + "theme": "outlined", + "url": "#test2" + } + ] + } + } + ] } } diff --git a/src/sub-blocks/LayoutItem/schema.ts b/src/sub-blocks/LayoutItem/schema.ts index cb0b4f321..50025887c 100644 --- a/src/sub-blocks/LayoutItem/schema.ts +++ b/src/sub-blocks/LayoutItem/schema.ts @@ -33,5 +33,9 @@ export const LayoutItem = { }, ], }, + controlPosition: { + type: 'string', + enum: ['content', 'footer'], + }, }, }; diff --git a/src/sub-blocks/PriceCard/PriceCard.scss b/src/sub-blocks/PriceCard/PriceCard.scss index 8a5c83d4d..703281fc9 100644 --- a/src/sub-blocks/PriceCard/PriceCard.scss +++ b/src/sub-blocks/PriceCard/PriceCard.scss @@ -108,16 +108,10 @@ $block: '.#{$ns}price-card'; &__links, &__buttons { margin-top: $indentSM; - - & > * { - &:not(:last-child) { - margin-right: $indentXS; - } - } } - &__link { - margin-top: 0; + &__footer { + margin-top: 0px; } @media (max-width: map-get($gridBreakpoints, 'md')) { diff --git a/src/sub-blocks/PriceCard/PriceCard.tsx b/src/sub-blocks/PriceCard/PriceCard.tsx index 42be66f19..2276d243d 100644 --- a/src/sub-blocks/PriceCard/PriceCard.tsx +++ b/src/sub-blocks/PriceCard/PriceCard.tsx @@ -2,14 +2,7 @@ import React from 'react'; import Check from '@gravity-ui/icons/Check'; -import { - BackgroundImage, - Button, - CardBase, - ContentList, - HTML, - Link as LinkBlock, -} from '../../components'; +import {BackgroundImage, Buttons, CardBase, ContentList, HTML, Links} from '../../components'; import {PriceCardProps} from '../../models'; import {block} from '../../utils'; @@ -33,7 +26,7 @@ const PriceCard = (props: PriceCardProps) => { } = props; return ( - +
@@ -62,25 +55,8 @@ const PriceCard = (props: PriceCardProps) => {
) : null}
- {buttons && ( -
- {buttons.map((button) => ( -
- )} - {links && ( -
- {links.map((link) => ( - - ))} -
- )} + +
diff --git a/src/sub-blocks/PriceCard/__stories__/PriceCard.stories.tsx b/src/sub-blocks/PriceCard/__stories__/PriceCard.stories.tsx index a02cc5990..02e24ef74 100644 --- a/src/sub-blocks/PriceCard/__stories__/PriceCard.stories.tsx +++ b/src/sub-blocks/PriceCard/__stories__/PriceCard.stories.tsx @@ -3,6 +3,10 @@ import React from 'react'; import {Meta, StoryFn} from '@storybook/react'; import {yfmTransform} from '../../../../.storybook/utils'; +import CardLayout from '../../../blocks/CardLayout/CardLayout'; +import {BlockBase} from '../../../components'; +import {ConstructorRow} from '../../../containers/PageConstructor/components/ConstructorRow'; +import {Grid} from '../../../grid'; import {PriceCardProps} from '../../../models'; import PriceCard from '../PriceCard'; @@ -56,13 +60,22 @@ const DifferentContentTemplate: StoryFn = (args) => { const MultipleItemsTemplate: StoryFn = (args) => { const items = data.themed.content as PriceCardProps[]; return ( -
- {items.map((itemArgs, index) => ( -
- -
- ))} -
+ + + + + {items.map((itemArgs, index) => ( + + ))} + + + + ); }; @@ -71,3 +84,7 @@ export const DifferentContent = DifferentContentTemplate.bind({}); export const Themed = MultipleItemsTemplate.bind({}); Default.args = data.default.content as PriceCardProps; +Themed.argTypes = { + theme: {table: {disable: true}}, + title: {table: {disable: true}}, +}; diff --git a/src/sub-blocks/PriceCard/schema.ts b/src/sub-blocks/PriceCard/schema.ts index 0a17d1c4d..260ce83df 100644 --- a/src/sub-blocks/PriceCard/schema.ts +++ b/src/sub-blocks/PriceCard/schema.ts @@ -41,6 +41,10 @@ export const PriceCardBlock = { type: 'string', }, }, + controlPosition: { + type: 'string', + enum: ['content', 'footer'], + }, }, }, }; diff --git a/src/sub-blocks/renderCardFooterControlsContainer/CardFooterControlsContainer.scss b/src/sub-blocks/renderCardFooterControlsContainer/CardFooterControlsContainer.scss new file mode 100644 index 000000000..557f7f2fb --- /dev/null +++ b/src/sub-blocks/renderCardFooterControlsContainer/CardFooterControlsContainer.scss @@ -0,0 +1,7 @@ +@import '../../../styles/variables.scss'; + +$block: '.#{$ns}card-footer-controls'; + +#{$block} { + margin-top: 0px; +} diff --git a/src/sub-blocks/renderCardFooterControlsContainer/renderCardFooterControlsContainer.tsx b/src/sub-blocks/renderCardFooterControlsContainer/renderCardFooterControlsContainer.tsx new file mode 100644 index 000000000..1941a5260 --- /dev/null +++ b/src/sub-blocks/renderCardFooterControlsContainer/renderCardFooterControlsContainer.tsx @@ -0,0 +1,12 @@ +import React from 'react'; + +import {CardBase} from '../../components'; +import {block} from '../../utils'; + +const b = block('card-footer-controls-containe'); + +const renderCardFooterControlsContainer = (children: React.ReactElement) => ( + {children} +); + +export default renderCardFooterControlsContainer; diff --git a/src/utils/renderContentControls/ContentControls.scss b/src/utils/renderContentControls/ContentControls.scss new file mode 100644 index 000000000..299cd924d --- /dev/null +++ b/src/utils/renderContentControls/ContentControls.scss @@ -0,0 +1,17 @@ +@import '../../../styles/variables.scss'; + +$block: '.#{$ns}content-controls'; + +#{$block} { + &__links, + &__buttons { + &_size { + &_s { + margin-top: $indentXS; + } + &_l { + margin-top: $indentSM; + } + } + } +} diff --git a/src/utils/renderContentControls/renderContentControls.tsx b/src/utils/renderContentControls/renderContentControls.tsx new file mode 100644 index 000000000..860aa3a6c --- /dev/null +++ b/src/utils/renderContentControls/renderContentControls.tsx @@ -0,0 +1,48 @@ +import React, {Fragment} from 'react'; + +import {Buttons, Links} from '../../components'; +import {ButtonProps, ContentSize, LinkProps} from '../../models'; +import {block} from '../../utils'; + +import './ContentControls.scss'; + +const b = block('content-controls'); + +type ContentControlsArgs = { + links?: LinkProps[]; + buttons?: ButtonProps[]; + titleId?: string; + size?: ContentSize; + qa?: Record; +}; +const renderContentControls = ( + {links, buttons, titleId, size = 's', qa = {}}: ContentControlsArgs, + renderContainer: (children: React.ReactElement) => React.ReactElement = (children) => children, +) => { + const {links: linksQa, link: linkQa, buttons: buttonsQa, button: buttonQa} = qa; + + return links || buttons + ? renderContainer( + + + + , + ) + : null; +}; + +export default renderContentControls;