@cloud-ru/uikit-product-page-layout 2.1.11 → 2.1.12-preview-72081ea7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/components/PageForm/PageForm.d.ts +3 -1
- package/dist/cjs/components/PageForm/PageForm.js +10 -6
- package/dist/cjs/components/PageForm/hooks.d.ts +9 -0
- package/dist/cjs/components/PageForm/hooks.js +22 -0
- package/dist/cjs/components/PageForm/styles.module.css +39 -0
- package/dist/esm/components/PageForm/PageForm.d.ts +3 -1
- package/dist/esm/components/PageForm/PageForm.js +12 -8
- package/dist/esm/components/PageForm/hooks.d.ts +9 -0
- package/dist/esm/components/PageForm/hooks.js +21 -0
- package/dist/esm/components/PageForm/styles.module.css +39 -0
- package/package.json +2 -2
- package/src/components/PageForm/PageForm.tsx +50 -29
- package/src/components/PageForm/hooks.tsx +29 -1
- package/src/components/PageForm/styles.module.scss +49 -0
|
@@ -6,6 +6,8 @@ import { HeadlineProps } from '../Headline';
|
|
|
6
6
|
import { ButtonPrimaryVariant, ButtonSecondaryVariant } from './types';
|
|
7
7
|
export type PageFormProps = WithSupportProps<PropsWithChildren<Pick<HeadlineProps, 'title' | 'subHeader'> & {
|
|
8
8
|
className?: string;
|
|
9
|
+
/** Закрепляет футер внизу формы при прокрутке контента */
|
|
10
|
+
stickyFooter?: boolean;
|
|
9
11
|
stepper?: ReactNode;
|
|
10
12
|
priceSummary?: {
|
|
11
13
|
total: ReactNode;
|
|
@@ -37,4 +39,4 @@ export type PageFormProps = WithSupportProps<PropsWithChildren<Pick<HeadlineProp
|
|
|
37
39
|
};
|
|
38
40
|
};
|
|
39
41
|
}>>;
|
|
40
|
-
export declare function PageForm({ children, stepper, title, subHeader, className, footer, sideBlock, priceSummary, ...rest }: PageFormProps): import("react/jsx-runtime").JSX.Element;
|
|
42
|
+
export declare function PageForm({ children, stepper, title, subHeader, className, footer, sideBlock, priceSummary, stickyFooter, ...rest }: PageFormProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -25,15 +25,19 @@ const hooks_1 = require("./hooks");
|
|
|
25
25
|
const styles_module_scss_1 = __importDefault(require('./styles.module.css'));
|
|
26
26
|
function PageForm(_a) {
|
|
27
27
|
var _b, _c;
|
|
28
|
-
var { children, stepper, title, subHeader, className, footer, sideBlock, priceSummary } = _a, rest = __rest(_a, ["children", "stepper", "title", "subHeader", "className", "footer", "sideBlock", "priceSummary"]);
|
|
28
|
+
var { children, stepper, title, subHeader, className, footer, sideBlock, priceSummary, stickyFooter } = _a, rest = __rest(_a, ["children", "stepper", "title", "subHeader", "className", "footer", "sideBlock", "priceSummary", "stickyFooter"]);
|
|
29
29
|
const getButtonLabel = (0, hooks_1.useGetButtonLabel)();
|
|
30
|
+
const { sentinelRef, atBottom } = (0, hooks_1.useStickyFooterShadow)(stickyFooter);
|
|
30
31
|
const moreItems = (0, react_1.useMemo)(() => [priceSummary === null || priceSummary === void 0 ? void 0 : priceSummary.content].concat(sideBlock === null || sideBlock === void 0 ? void 0 : sideBlock.map(item => item.content)).filter(Boolean), [priceSummary === null || priceSummary === void 0 ? void 0 : priceSummary.content, sideBlock]);
|
|
31
32
|
const PrimaryButton = (0, hooks_1.useButtonWithTooltip)({ Button: button_1.ButtonFilled, tooltip: footer === null || footer === void 0 ? void 0 : footer.buttonPrimary.tooltip });
|
|
32
33
|
const SecondaryButton = (0, hooks_1.useButtonWithTooltip)({ Button: button_1.ButtonOutline, tooltip: (_b = footer === null || footer === void 0 ? void 0 : footer.buttonSecondary) === null || _b === void 0 ? void 0 : _b.tooltip });
|
|
33
34
|
const AdditionalButton = (0, hooks_1.useButtonWithTooltip)({ Button: button_1.ButtonSimple, tooltip: (_c = footer === null || footer === void 0 ? void 0 : footer.buttonAdditional) === null || _c === void 0 ? void 0 : _c.tooltip });
|
|
34
|
-
return ((0, jsx_runtime_1.jsxs)("div", Object.assign({ className: (0, classnames_1.default)(styles_module_scss_1.default.container, className) }, (0, uikit_product_utils_1.extractSupportProps)(rest), { children: [(0, jsx_runtime_1.jsxs)("div", { className: styles_module_scss_1.default.form, children: [(0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_1.default.headline, children: (0, jsx_runtime_1.jsx)(Headline_1.Headline, { title: title, subHeader: subHeader }) }), stepper, (0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_1.default.body, children: children }), footer && ((0, jsx_runtime_1.jsxs)(
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
return ((0, jsx_runtime_1.jsxs)("div", Object.assign({ className: (0, classnames_1.default)(styles_module_scss_1.default.container, className) }, (0, uikit_product_utils_1.extractSupportProps)(rest), { children: [(0, jsx_runtime_1.jsxs)("div", { className: (0, classnames_1.default)(styles_module_scss_1.default.form, { [styles_module_scss_1.default.formStickyFooter]: stickyFooter && footer }), children: [(0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_1.default.headline, children: (0, jsx_runtime_1.jsx)(Headline_1.Headline, { title: title, subHeader: subHeader }) }), stepper, (0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_1.default.body, children: children }), footer && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("div", { className: (0, classnames_1.default)(styles_module_scss_1.default.footer, {
|
|
36
|
+
[styles_module_scss_1.default.footerSticky]: stickyFooter,
|
|
37
|
+
[styles_module_scss_1.default.footerStuck]: stickyFooter && !atBottom,
|
|
38
|
+
}), children: [footer.buttonSecondary && ((0, jsx_runtime_1.jsx)(SecondaryButton, Object.assign({}, footer.buttonSecondary, { size: 'm', appearance: 'neutral', label: footer.buttonSecondary.variant === 'custom'
|
|
39
|
+
? footer.buttonSecondary.label
|
|
40
|
+
: getButtonLabel(footer.buttonSecondary.variant) }))), (0, jsx_runtime_1.jsxs)("div", { className: styles_module_scss_1.default.mainActions, children: [footer.buttonAdditional && (0, jsx_runtime_1.jsx)(AdditionalButton, Object.assign({}, footer.buttonAdditional, { size: 'm' })), (0, jsx_runtime_1.jsx)(PrimaryButton, Object.assign({}, footer.buttonPrimary, { size: 'm', label: footer.buttonPrimary.variant === 'custom'
|
|
41
|
+
? footer.buttonPrimary.label
|
|
42
|
+
: getButtonLabel(footer.buttonPrimary.variant) }))] })] }), stickyFooter && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("div", { ref: sentinelRef, className: styles_module_scss_1.default.footerSentinel, "aria-hidden": true }), (0, jsx_runtime_1.jsx)("div", {})] }))] }))] }), (moreItems === null || moreItems === void 0 ? void 0 : moreItems.length) > 0 && (0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_1.default.sideItems, children: moreItems })] })));
|
|
39
43
|
}
|
|
@@ -5,4 +5,13 @@ export declare function useButtonWithTooltip<T extends Record<string, unknown>>(
|
|
|
5
5
|
tooltip?: TooltipProps;
|
|
6
6
|
Button: ForwardRefExoticComponent<T>;
|
|
7
7
|
}): ForwardRefExoticComponent<T> | ((props: T) => import("react/jsx-runtime").JSX.Element);
|
|
8
|
+
/**
|
|
9
|
+
* Показывает тень у прилипшего футера, только пока под ним есть прокручиваемый контент.
|
|
10
|
+
* Отслеживает «сентинел» внизу формы: как только он попадает во вьюпорт (доскроллили до конца),
|
|
11
|
+
* тень скрывается.
|
|
12
|
+
*/
|
|
13
|
+
export declare function useStickyFooterShadow(enabled?: boolean): {
|
|
14
|
+
sentinelRef: import("react").RefObject<HTMLDivElement>;
|
|
15
|
+
atBottom: boolean;
|
|
16
|
+
};
|
|
8
17
|
export declare function useGetButtonLabel(): (variant: ButtonPrimaryVariant | ButtonSecondaryVariant) => string;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.useButtonWithTooltip = useButtonWithTooltip;
|
|
4
|
+
exports.useStickyFooterShadow = useStickyFooterShadow;
|
|
4
5
|
exports.useGetButtonLabel = useGetButtonLabel;
|
|
5
6
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
7
|
+
const react_1 = require("react");
|
|
6
8
|
const uikit_product_locale_1 = require("@cloud-ru/uikit-product-locale");
|
|
7
9
|
const tooltip_1 = require("@snack-uikit/tooltip");
|
|
8
10
|
function useButtonWithTooltip({ Button, tooltip, }) {
|
|
@@ -13,6 +15,26 @@ function useButtonWithTooltip({ Button, tooltip, }) {
|
|
|
13
15
|
}
|
|
14
16
|
return Button;
|
|
15
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* Показывает тень у прилипшего футера, только пока под ним есть прокручиваемый контент.
|
|
20
|
+
* Отслеживает «сентинел» внизу формы: как только он попадает во вьюпорт (доскроллили до конца),
|
|
21
|
+
* тень скрывается.
|
|
22
|
+
*/
|
|
23
|
+
function useStickyFooterShadow(enabled) {
|
|
24
|
+
const sentinelRef = (0, react_1.useRef)(null);
|
|
25
|
+
const [atBottom, setAtBottom] = (0, react_1.useState)(false);
|
|
26
|
+
(0, react_1.useEffect)(() => {
|
|
27
|
+
const sentinel = sentinelRef.current;
|
|
28
|
+
if (!enabled || !sentinel || typeof IntersectionObserver === 'undefined') {
|
|
29
|
+
setAtBottom(false);
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
const observer = new IntersectionObserver(([entry]) => setAtBottom(entry.isIntersecting), { threshold: 1 });
|
|
33
|
+
observer.observe(sentinel);
|
|
34
|
+
return () => observer.disconnect();
|
|
35
|
+
}, [enabled]);
|
|
36
|
+
return { sentinelRef, atBottom };
|
|
37
|
+
}
|
|
16
38
|
function useGetButtonLabel() {
|
|
17
39
|
const { t } = (0, uikit_product_locale_1.useLocale)('PageLayout');
|
|
18
40
|
return function getButtonLabel(variant) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
@charset "UTF-8";
|
|
1
2
|
.container{
|
|
2
3
|
padding:var(--dimension-3m, 24px);
|
|
3
4
|
min-width:-moz-fit-content;
|
|
@@ -20,6 +21,7 @@
|
|
|
20
21
|
border-radius:16px;
|
|
21
22
|
min-height:100%;
|
|
22
23
|
box-sizing:border-box;
|
|
24
|
+
position:relative;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
.headline{
|
|
@@ -32,6 +34,43 @@
|
|
|
32
34
|
align-items:center;
|
|
33
35
|
}
|
|
34
36
|
|
|
37
|
+
.footerSticky{
|
|
38
|
+
position:sticky;
|
|
39
|
+
bottom:0;
|
|
40
|
+
box-sizing:border-box;
|
|
41
|
+
background-color:var(--sys-neutral-background1-level, #fdfdfd);
|
|
42
|
+
border-radius:16px;
|
|
43
|
+
padding:24px 56px;
|
|
44
|
+
margin:-24px -56px -56px;
|
|
45
|
+
transition:border-radius 0.15s ease;
|
|
46
|
+
}
|
|
47
|
+
.footerSticky::before{
|
|
48
|
+
content:"";
|
|
49
|
+
position:absolute;
|
|
50
|
+
inset:0;
|
|
51
|
+
border-radius:inherit;
|
|
52
|
+
box-shadow:var(--box-shadow-elevation-level3, 0px 0px 8px 0px rgba(0, 0, 0, 0.0784313725), 0px 8px 16px 0px rgba(0, 0, 0, 0.0784313725));
|
|
53
|
+
clip-path:inset(-24px -8px calc(100% - 16px) -8px);
|
|
54
|
+
opacity:0;
|
|
55
|
+
pointer-events:none;
|
|
56
|
+
transition:opacity 0.15s ease;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.footerStuck{
|
|
60
|
+
border-radius:16px 16px 0 0;
|
|
61
|
+
}
|
|
62
|
+
.footerStuck::before{
|
|
63
|
+
opacity:1;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.footerSentinel{
|
|
67
|
+
position:absolute;
|
|
68
|
+
bottom:0;
|
|
69
|
+
height:1px;
|
|
70
|
+
width:1px;
|
|
71
|
+
pointer-events:none;
|
|
72
|
+
}
|
|
73
|
+
|
|
35
74
|
.mainActions{
|
|
36
75
|
display:flex;
|
|
37
76
|
align-items:center;
|
|
@@ -6,6 +6,8 @@ import { HeadlineProps } from '../Headline';
|
|
|
6
6
|
import { ButtonPrimaryVariant, ButtonSecondaryVariant } from './types';
|
|
7
7
|
export type PageFormProps = WithSupportProps<PropsWithChildren<Pick<HeadlineProps, 'title' | 'subHeader'> & {
|
|
8
8
|
className?: string;
|
|
9
|
+
/** Закрепляет футер внизу формы при прокрутке контента */
|
|
10
|
+
stickyFooter?: boolean;
|
|
9
11
|
stepper?: ReactNode;
|
|
10
12
|
priceSummary?: {
|
|
11
13
|
total: ReactNode;
|
|
@@ -37,4 +39,4 @@ export type PageFormProps = WithSupportProps<PropsWithChildren<Pick<HeadlineProp
|
|
|
37
39
|
};
|
|
38
40
|
};
|
|
39
41
|
}>>;
|
|
40
|
-
export declare function PageForm({ children, stepper, title, subHeader, className, footer, sideBlock, priceSummary, ...rest }: PageFormProps): import("react/jsx-runtime").JSX.Element;
|
|
42
|
+
export declare function PageForm({ children, stepper, title, subHeader, className, footer, sideBlock, priceSummary, stickyFooter, ...rest }: PageFormProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -9,25 +9,29 @@ var __rest = (this && this.__rest) || function (s, e) {
|
|
|
9
9
|
}
|
|
10
10
|
return t;
|
|
11
11
|
};
|
|
12
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
12
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
13
13
|
import cn from 'classnames';
|
|
14
14
|
import { useMemo } from 'react';
|
|
15
15
|
import { extractSupportProps } from '@cloud-ru/uikit-product-utils';
|
|
16
16
|
import { ButtonFilled, ButtonOutline, ButtonSimple, } from '@snack-uikit/button';
|
|
17
17
|
import { Headline } from '../Headline';
|
|
18
|
-
import { useButtonWithTooltip, useGetButtonLabel } from './hooks';
|
|
18
|
+
import { useButtonWithTooltip, useGetButtonLabel, useStickyFooterShadow } from './hooks';
|
|
19
19
|
import styles from './styles.module.css';
|
|
20
20
|
export function PageForm(_a) {
|
|
21
21
|
var _b, _c;
|
|
22
|
-
var { children, stepper, title, subHeader, className, footer, sideBlock, priceSummary } = _a, rest = __rest(_a, ["children", "stepper", "title", "subHeader", "className", "footer", "sideBlock", "priceSummary"]);
|
|
22
|
+
var { children, stepper, title, subHeader, className, footer, sideBlock, priceSummary, stickyFooter } = _a, rest = __rest(_a, ["children", "stepper", "title", "subHeader", "className", "footer", "sideBlock", "priceSummary", "stickyFooter"]);
|
|
23
23
|
const getButtonLabel = useGetButtonLabel();
|
|
24
|
+
const { sentinelRef, atBottom } = useStickyFooterShadow(stickyFooter);
|
|
24
25
|
const moreItems = useMemo(() => [priceSummary === null || priceSummary === void 0 ? void 0 : priceSummary.content].concat(sideBlock === null || sideBlock === void 0 ? void 0 : sideBlock.map(item => item.content)).filter(Boolean), [priceSummary === null || priceSummary === void 0 ? void 0 : priceSummary.content, sideBlock]);
|
|
25
26
|
const PrimaryButton = useButtonWithTooltip({ Button: ButtonFilled, tooltip: footer === null || footer === void 0 ? void 0 : footer.buttonPrimary.tooltip });
|
|
26
27
|
const SecondaryButton = useButtonWithTooltip({ Button: ButtonOutline, tooltip: (_b = footer === null || footer === void 0 ? void 0 : footer.buttonSecondary) === null || _b === void 0 ? void 0 : _b.tooltip });
|
|
27
28
|
const AdditionalButton = useButtonWithTooltip({ Button: ButtonSimple, tooltip: (_c = footer === null || footer === void 0 ? void 0 : footer.buttonAdditional) === null || _c === void 0 ? void 0 : _c.tooltip });
|
|
28
|
-
return (_jsxs("div", Object.assign({ className: cn(styles.container, className) }, extractSupportProps(rest), { children: [_jsxs("div", { className: styles.form, children: [_jsx("div", { className: styles.headline, children: _jsx(Headline, { title: title, subHeader: subHeader }) }), stepper, _jsx("div", { className: styles.body, children: children }), footer && (_jsxs(
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
return (_jsxs("div", Object.assign({ className: cn(styles.container, className) }, extractSupportProps(rest), { children: [_jsxs("div", { className: cn(styles.form, { [styles.formStickyFooter]: stickyFooter && footer }), children: [_jsx("div", { className: styles.headline, children: _jsx(Headline, { title: title, subHeader: subHeader }) }), stepper, _jsx("div", { className: styles.body, children: children }), footer && (_jsxs(_Fragment, { children: [_jsxs("div", { className: cn(styles.footer, {
|
|
30
|
+
[styles.footerSticky]: stickyFooter,
|
|
31
|
+
[styles.footerStuck]: stickyFooter && !atBottom,
|
|
32
|
+
}), children: [footer.buttonSecondary && (_jsx(SecondaryButton, Object.assign({}, footer.buttonSecondary, { size: 'm', appearance: 'neutral', label: footer.buttonSecondary.variant === 'custom'
|
|
33
|
+
? footer.buttonSecondary.label
|
|
34
|
+
: getButtonLabel(footer.buttonSecondary.variant) }))), _jsxs("div", { className: styles.mainActions, children: [footer.buttonAdditional && _jsx(AdditionalButton, Object.assign({}, footer.buttonAdditional, { size: 'm' })), _jsx(PrimaryButton, Object.assign({}, footer.buttonPrimary, { size: 'm', label: footer.buttonPrimary.variant === 'custom'
|
|
35
|
+
? footer.buttonPrimary.label
|
|
36
|
+
: getButtonLabel(footer.buttonPrimary.variant) }))] })] }), stickyFooter && (_jsxs(_Fragment, { children: [_jsx("div", { ref: sentinelRef, className: styles.footerSentinel, "aria-hidden": true }), _jsx("div", {})] }))] }))] }), (moreItems === null || moreItems === void 0 ? void 0 : moreItems.length) > 0 && _jsx("div", { className: styles.sideItems, children: moreItems })] })));
|
|
33
37
|
}
|
|
@@ -5,4 +5,13 @@ export declare function useButtonWithTooltip<T extends Record<string, unknown>>(
|
|
|
5
5
|
tooltip?: TooltipProps;
|
|
6
6
|
Button: ForwardRefExoticComponent<T>;
|
|
7
7
|
}): ForwardRefExoticComponent<T> | ((props: T) => import("react/jsx-runtime").JSX.Element);
|
|
8
|
+
/**
|
|
9
|
+
* Показывает тень у прилипшего футера, только пока под ним есть прокручиваемый контент.
|
|
10
|
+
* Отслеживает «сентинел» внизу формы: как только он попадает во вьюпорт (доскроллили до конца),
|
|
11
|
+
* тень скрывается.
|
|
12
|
+
*/
|
|
13
|
+
export declare function useStickyFooterShadow(enabled?: boolean): {
|
|
14
|
+
sentinelRef: import("react").RefObject<HTMLDivElement>;
|
|
15
|
+
atBottom: boolean;
|
|
16
|
+
};
|
|
8
17
|
export declare function useGetButtonLabel(): (variant: ButtonPrimaryVariant | ButtonSecondaryVariant) => string;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
3
|
import { useLocale } from '@cloud-ru/uikit-product-locale';
|
|
3
4
|
import { Tooltip } from '@snack-uikit/tooltip';
|
|
4
5
|
export function useButtonWithTooltip({ Button, tooltip, }) {
|
|
@@ -9,6 +10,26 @@ export function useButtonWithTooltip({ Button, tooltip, }) {
|
|
|
9
10
|
}
|
|
10
11
|
return Button;
|
|
11
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* Показывает тень у прилипшего футера, только пока под ним есть прокручиваемый контент.
|
|
15
|
+
* Отслеживает «сентинел» внизу формы: как только он попадает во вьюпорт (доскроллили до конца),
|
|
16
|
+
* тень скрывается.
|
|
17
|
+
*/
|
|
18
|
+
export function useStickyFooterShadow(enabled) {
|
|
19
|
+
const sentinelRef = useRef(null);
|
|
20
|
+
const [atBottom, setAtBottom] = useState(false);
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
const sentinel = sentinelRef.current;
|
|
23
|
+
if (!enabled || !sentinel || typeof IntersectionObserver === 'undefined') {
|
|
24
|
+
setAtBottom(false);
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
const observer = new IntersectionObserver(([entry]) => setAtBottom(entry.isIntersecting), { threshold: 1 });
|
|
28
|
+
observer.observe(sentinel);
|
|
29
|
+
return () => observer.disconnect();
|
|
30
|
+
}, [enabled]);
|
|
31
|
+
return { sentinelRef, atBottom };
|
|
32
|
+
}
|
|
12
33
|
export function useGetButtonLabel() {
|
|
13
34
|
const { t } = useLocale('PageLayout');
|
|
14
35
|
return function getButtonLabel(variant) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
@charset "UTF-8";
|
|
1
2
|
.container{
|
|
2
3
|
padding:var(--dimension-3m, 24px);
|
|
3
4
|
min-width:-moz-fit-content;
|
|
@@ -20,6 +21,7 @@
|
|
|
20
21
|
border-radius:16px;
|
|
21
22
|
min-height:100%;
|
|
22
23
|
box-sizing:border-box;
|
|
24
|
+
position:relative;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
.headline{
|
|
@@ -32,6 +34,43 @@
|
|
|
32
34
|
align-items:center;
|
|
33
35
|
}
|
|
34
36
|
|
|
37
|
+
.footerSticky{
|
|
38
|
+
position:sticky;
|
|
39
|
+
bottom:0;
|
|
40
|
+
box-sizing:border-box;
|
|
41
|
+
background-color:var(--sys-neutral-background1-level, #fdfdfd);
|
|
42
|
+
border-radius:16px;
|
|
43
|
+
padding:24px 56px;
|
|
44
|
+
margin:-24px -56px -56px;
|
|
45
|
+
transition:border-radius 0.15s ease;
|
|
46
|
+
}
|
|
47
|
+
.footerSticky::before{
|
|
48
|
+
content:"";
|
|
49
|
+
position:absolute;
|
|
50
|
+
inset:0;
|
|
51
|
+
border-radius:inherit;
|
|
52
|
+
box-shadow:var(--box-shadow-elevation-level3, 0px 0px 8px 0px rgba(0, 0, 0, 0.0784313725), 0px 8px 16px 0px rgba(0, 0, 0, 0.0784313725));
|
|
53
|
+
clip-path:inset(-24px -8px calc(100% - 16px) -8px);
|
|
54
|
+
opacity:0;
|
|
55
|
+
pointer-events:none;
|
|
56
|
+
transition:opacity 0.15s ease;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.footerStuck{
|
|
60
|
+
border-radius:16px 16px 0 0;
|
|
61
|
+
}
|
|
62
|
+
.footerStuck::before{
|
|
63
|
+
opacity:1;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.footerSentinel{
|
|
67
|
+
position:absolute;
|
|
68
|
+
bottom:0;
|
|
69
|
+
height:1px;
|
|
70
|
+
width:1px;
|
|
71
|
+
pointer-events:none;
|
|
72
|
+
}
|
|
73
|
+
|
|
35
74
|
.mainActions{
|
|
36
75
|
display:flex;
|
|
37
76
|
align-items:center;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloud-ru/uikit-product-page-layout",
|
|
3
3
|
"title": "Page Layout",
|
|
4
|
-
"version": "2.1.
|
|
4
|
+
"version": "2.1.12-preview-72081ea7.0",
|
|
5
5
|
"sideEffects": [
|
|
6
6
|
"*.css",
|
|
7
7
|
"*.woff",
|
|
@@ -57,5 +57,5 @@
|
|
|
57
57
|
"peerDependencies": {
|
|
58
58
|
"@cloud-ru/uikit-product-locale": "*"
|
|
59
59
|
},
|
|
60
|
-
"gitHead": "
|
|
60
|
+
"gitHead": "b723e716054c626e1b442760c79638bacfcad729"
|
|
61
61
|
}
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
import { TooltipProps } from '@snack-uikit/tooltip';
|
|
14
14
|
|
|
15
15
|
import { Headline, HeadlineProps } from '../Headline';
|
|
16
|
-
import { useButtonWithTooltip, useGetButtonLabel } from './hooks';
|
|
16
|
+
import { useButtonWithTooltip, useGetButtonLabel, useStickyFooterShadow } from './hooks';
|
|
17
17
|
import styles from './styles.module.scss';
|
|
18
18
|
import { ButtonPrimaryVariant, ButtonSecondaryVariant } from './types';
|
|
19
19
|
|
|
@@ -22,6 +22,9 @@ export type PageFormProps = WithSupportProps<
|
|
|
22
22
|
Pick<HeadlineProps, 'title' | 'subHeader'> & {
|
|
23
23
|
className?: string;
|
|
24
24
|
|
|
25
|
+
/** Закрепляет футер внизу формы при прокрутке контента */
|
|
26
|
+
stickyFooter?: boolean;
|
|
27
|
+
|
|
25
28
|
stepper?: ReactNode;
|
|
26
29
|
|
|
27
30
|
priceSummary?: {
|
|
@@ -74,10 +77,13 @@ export function PageForm({
|
|
|
74
77
|
footer,
|
|
75
78
|
sideBlock,
|
|
76
79
|
priceSummary,
|
|
80
|
+
stickyFooter,
|
|
77
81
|
...rest
|
|
78
82
|
}: PageFormProps) {
|
|
79
83
|
const getButtonLabel = useGetButtonLabel();
|
|
80
84
|
|
|
85
|
+
const { sentinelRef, atBottom } = useStickyFooterShadow(stickyFooter);
|
|
86
|
+
|
|
81
87
|
const moreItems = useMemo(
|
|
82
88
|
() => [priceSummary?.content].concat(sideBlock?.map(item => item.content)).filter(Boolean),
|
|
83
89
|
[priceSummary?.content, sideBlock],
|
|
@@ -89,7 +95,7 @@ export function PageForm({
|
|
|
89
95
|
|
|
90
96
|
return (
|
|
91
97
|
<div className={cn(styles.container, className)} {...extractSupportProps(rest)}>
|
|
92
|
-
<div className={styles.form}>
|
|
98
|
+
<div className={cn(styles.form, { [styles.formStickyFooter]: stickyFooter && footer })}>
|
|
93
99
|
<div className={styles.headline}>
|
|
94
100
|
<Headline title={title} subHeader={subHeader} />
|
|
95
101
|
</div>
|
|
@@ -99,34 +105,49 @@ export function PageForm({
|
|
|
99
105
|
<div className={styles.body}>{children}</div>
|
|
100
106
|
|
|
101
107
|
{footer && (
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
108
|
+
<>
|
|
109
|
+
<div
|
|
110
|
+
className={cn(styles.footer, {
|
|
111
|
+
[styles.footerSticky]: stickyFooter,
|
|
112
|
+
[styles.footerStuck]: stickyFooter && !atBottom,
|
|
113
|
+
})}
|
|
114
|
+
>
|
|
115
|
+
{footer.buttonSecondary && (
|
|
116
|
+
<SecondaryButton
|
|
117
|
+
{...footer.buttonSecondary}
|
|
118
|
+
size='m'
|
|
119
|
+
appearance='neutral'
|
|
120
|
+
label={
|
|
121
|
+
footer.buttonSecondary.variant === 'custom'
|
|
122
|
+
? footer.buttonSecondary.label
|
|
123
|
+
: getButtonLabel(footer.buttonSecondary.variant)
|
|
124
|
+
}
|
|
125
|
+
/>
|
|
126
|
+
)}
|
|
127
|
+
|
|
128
|
+
<div className={styles.mainActions}>
|
|
129
|
+
{footer.buttonAdditional && <AdditionalButton {...footer.buttonAdditional} size='m' />}
|
|
130
|
+
|
|
131
|
+
<PrimaryButton
|
|
132
|
+
{...footer.buttonPrimary}
|
|
133
|
+
size='m'
|
|
134
|
+
label={
|
|
135
|
+
footer.buttonPrimary.variant === 'custom'
|
|
136
|
+
? footer.buttonPrimary.label
|
|
137
|
+
: getButtonLabel(footer.buttonPrimary.variant)
|
|
138
|
+
}
|
|
139
|
+
/>
|
|
140
|
+
</div>
|
|
128
141
|
</div>
|
|
129
|
-
|
|
142
|
+
|
|
143
|
+
{stickyFooter && (
|
|
144
|
+
<>
|
|
145
|
+
<div ref={sentinelRef} className={styles.footerSentinel} aria-hidden />
|
|
146
|
+
{/* Нужно для правильного отступа снизу */}
|
|
147
|
+
<div />
|
|
148
|
+
</>
|
|
149
|
+
)}
|
|
150
|
+
</>
|
|
130
151
|
)}
|
|
131
152
|
</div>
|
|
132
153
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ForwardRefExoticComponent } from 'react';
|
|
1
|
+
import { ForwardRefExoticComponent, useEffect, useRef, useState } from 'react';
|
|
2
2
|
|
|
3
3
|
import { useLocale } from '@cloud-ru/uikit-product-locale';
|
|
4
4
|
import { Tooltip, TooltipProps } from '@snack-uikit/tooltip';
|
|
@@ -25,6 +25,34 @@ export function useButtonWithTooltip<T extends Record<string, unknown>>({
|
|
|
25
25
|
return Button;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Показывает тень у прилипшего футера, только пока под ним есть прокручиваемый контент.
|
|
30
|
+
* Отслеживает «сентинел» внизу формы: как только он попадает во вьюпорт (доскроллили до конца),
|
|
31
|
+
* тень скрывается.
|
|
32
|
+
*/
|
|
33
|
+
export function useStickyFooterShadow(enabled?: boolean) {
|
|
34
|
+
const sentinelRef = useRef<HTMLDivElement>(null);
|
|
35
|
+
const [atBottom, setAtBottom] = useState(false);
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
const sentinel = sentinelRef.current;
|
|
39
|
+
|
|
40
|
+
if (!enabled || !sentinel || typeof IntersectionObserver === 'undefined') {
|
|
41
|
+
setAtBottom(false);
|
|
42
|
+
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const observer = new IntersectionObserver(([entry]) => setAtBottom(entry.isIntersecting), { threshold: 1 });
|
|
47
|
+
|
|
48
|
+
observer.observe(sentinel);
|
|
49
|
+
|
|
50
|
+
return () => observer.disconnect();
|
|
51
|
+
}, [enabled]);
|
|
52
|
+
|
|
53
|
+
return { sentinelRef, atBottom };
|
|
54
|
+
}
|
|
55
|
+
|
|
28
56
|
export function useGetButtonLabel() {
|
|
29
57
|
const { t } = useLocale('PageLayout');
|
|
30
58
|
|
|
@@ -23,6 +23,7 @@ $prefix-height: styles-theme-variables.simple-var(styles-theme-variables.$sans-h
|
|
|
23
23
|
border-radius: 16px;
|
|
24
24
|
min-height: 100%;
|
|
25
25
|
box-sizing: border-box;
|
|
26
|
+
position: relative;
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
.headline {
|
|
@@ -35,6 +36,54 @@ $prefix-height: styles-theme-variables.simple-var(styles-theme-variables.$sans-h
|
|
|
35
36
|
align-items: center;
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
.footerSticky {
|
|
40
|
+
position: sticky;
|
|
41
|
+
bottom: 0;
|
|
42
|
+
|
|
43
|
+
box-sizing: border-box;
|
|
44
|
+
background-color: styles-theme-variables.$sys-neutral-background1-level;
|
|
45
|
+
border-radius: 16px;
|
|
46
|
+
padding: 24px 56px;
|
|
47
|
+
margin: -24px -56px -56px;
|
|
48
|
+
|
|
49
|
+
/* stylelint-disable-next-line declaration-property-value-allowed-list */
|
|
50
|
+
transition: border-radius 0.15s ease;
|
|
51
|
+
|
|
52
|
+
/* Тень из токена, но обрезаем всё ниже верхней кромки — остаётся только тень сверху. */
|
|
53
|
+
&::before {
|
|
54
|
+
content: '';
|
|
55
|
+
position: absolute;
|
|
56
|
+
inset: 0;
|
|
57
|
+
border-radius: inherit;
|
|
58
|
+
box-shadow: styles-theme-variables.$box-shadow-elevation-level3;
|
|
59
|
+
|
|
60
|
+
/* оставляем полосу над верхней кромкой + зону скруглённых углов (до высоты радиуса),
|
|
61
|
+
по бокам чуть выходим наружу, чтобы поймать тень на скруглении */
|
|
62
|
+
clip-path: inset(-24px -8px calc(100% - 16px) -8px);
|
|
63
|
+
opacity: 0;
|
|
64
|
+
pointer-events: none;
|
|
65
|
+
|
|
66
|
+
/* stylelint-disable-next-line declaration-property-value-allowed-list */
|
|
67
|
+
transition: opacity 0.15s ease;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.footerStuck {
|
|
72
|
+
border-radius: 16px 16px 0 0;
|
|
73
|
+
|
|
74
|
+
&::before {
|
|
75
|
+
opacity: 1;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.footerSentinel {
|
|
80
|
+
position: absolute;
|
|
81
|
+
bottom: 0;
|
|
82
|
+
height: 1px;
|
|
83
|
+
width: 1px;
|
|
84
|
+
pointer-events: none;
|
|
85
|
+
}
|
|
86
|
+
|
|
38
87
|
.mainActions {
|
|
39
88
|
display: flex;
|
|
40
89
|
align-items: center;
|