diff --git a/.storybook/image-snapshots/expected/components_FullscreenModal_Single Column (layout single-6).png b/.storybook/image-snapshots/expected/components_FullscreenModal_Single Column (layout single-6).png new file mode 100644 index 000000000..12afe607b Binary files /dev/null and b/.storybook/image-snapshots/expected/components_FullscreenModal_Single Column (layout single-6).png differ diff --git a/.storybook/image-snapshots/expected/components_FullscreenModal_Single Column (layout single-8).png b/.storybook/image-snapshots/expected/components_FullscreenModal_Single Column (layout single-8).png new file mode 100644 index 000000000..bc589df65 Binary files /dev/null and b/.storybook/image-snapshots/expected/components_FullscreenModal_Single Column (layout single-8).png differ diff --git a/.storybook/image-snapshots/expected/components_FullscreenModal_Two Columns with sidebar (layout sidebar-4-6).png b/.storybook/image-snapshots/expected/components_FullscreenModal_Two Columns with sidebar (layout sidebar-4-6).png new file mode 100644 index 000000000..19896de6b Binary files /dev/null and b/.storybook/image-snapshots/expected/components_FullscreenModal_Two Columns with sidebar (layout sidebar-4-6).png differ diff --git a/.storybook/image-snapshots/expected/components_FullscreenModal_Two Columns with sidebar (layout sidebar-4-8).png b/.storybook/image-snapshots/expected/components_FullscreenModal_Two Columns with sidebar (layout sidebar-4-8).png new file mode 100644 index 000000000..bdd54701e Binary files /dev/null and b/.storybook/image-snapshots/expected/components_FullscreenModal_Two Columns with sidebar (layout sidebar-4-8).png differ diff --git a/.storybook/image-snapshots/expected/components_FullscreenModal_With Long Content.png b/.storybook/image-snapshots/expected/components_FullscreenModal_With Long Content.png new file mode 100644 index 000000000..01bdc805d Binary files /dev/null and b/.storybook/image-snapshots/expected/components_FullscreenModal_With Long Content.png differ diff --git a/.storybook/main.ts b/.storybook/main.ts index 2224cec0d..9e506ae85 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -8,6 +8,7 @@ module.exports = { '@storybook/addon-backgrounds', '@storybook/addon-docs', '@storybook/addon-controls', + 'storycap' ], typescript: { check: false, diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index ba6550425..802399ce5 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { withScreenshot } from 'storycap' import { DSProvider, createIconLibrary } from '../src/theme'; import colors from '../src/theme/colors'; @@ -37,6 +38,10 @@ export const parameters = { controls: { expanded: true, hideNoControlsWarning: true }, docs: { source: { type: 'dynamic' } }, actions: { argTypesRegex: '^on.*' }, + // storycap settings + screenshot: { + fullPage: true, + }, }; createIconLibrary(); @@ -47,4 +52,4 @@ const wrapper = (storyFn) => ( ); -export const decorators = [wrapper] +export const decorators = [ withScreenshot, wrapper ]; diff --git a/package.json b/package.json index d9dc0e11a..8171ceb76 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "@types/react": "^16.9.38", "@types/styled-components": "^5.1.0", "csstype": "^3.0.0-beta.4", + "lodash.debounce": "^4.0.8", "polished": "^3.6.5", "prop-types": "^15.7.2", "ramda": "^0.27.0", diff --git a/src/components/FullscreenModal/Footer/Footer.tsx b/src/components/FullscreenModal/Footer/Footer.tsx new file mode 100644 index 000000000..a1c0b0f3a --- /dev/null +++ b/src/components/FullscreenModal/Footer/Footer.tsx @@ -0,0 +1,85 @@ +import React, { useRef } from 'react'; +import PropTypes from 'prop-types'; +import styled from 'styled-components'; + +import { getColor, pxToRem } from '../../../utils/helpers'; +import { FlexContainer } from '../../FlexContainer'; +import { ScrollToTopButton } from '../ScrollToTopButton'; +import { useStickyFooter } from '../hooks/useStickyFooter'; +import { Col, Container, Row } from '../../layout'; +import { FooterProps } from './Footer.types'; + +const BaseStickyFooter = styled.footer` + position: fixed; + bottom: 0; + left: 0; + width: 100%; + padding: ${pxToRem(10, 0)}; + background-color: ${getColor('graphite5H')}; + border-top: 1px solid ${getColor('graphiteHB')}; +`; +const BaseFooter = styled.footer` + border-top: 1px solid ${getColor('graphiteHB')}; + padding-top: ${pxToRem(24)}; + margin-top: ${pxToRem(40)}; +`; + +const Footer: React.FC = ({ + children, + width, + offset, + modalRef, + scrollToTopButtonLabel, +}) => { + const modalFooterRef = useRef(null); + const { isFixed, shouldShowScrollToTopButton } = useStickyFooter( + modalRef, + modalFooterRef, + ); + + const scrollToTop = () => { + modalRef.current.scrollTo(0, 0); + }; + + return ( + <> + {isFixed && ( + + + + + {children} + + + + + )} + + {children} + {shouldShowScrollToTopButton && ( + + + + )} + + + ); +}; + +Footer.propTypes = { + width: PropTypes.number.isRequired, + offset: PropTypes.number.isRequired, + modalRef: PropTypes.exact({ + current: PropTypes.instanceOf(HTMLElement), + }).isRequired, + scrollToTopButtonLabel: PropTypes.string, +}; + +export default Footer; diff --git a/src/components/FullscreenModal/Footer/Footer.types.ts b/src/components/FullscreenModal/Footer/Footer.types.ts new file mode 100644 index 000000000..7f6c1b6b4 --- /dev/null +++ b/src/components/FullscreenModal/Footer/Footer.types.ts @@ -0,0 +1,6 @@ +export interface FooterProps { + width: number; + offset: number; + modalRef: React.MutableRefObject; + scrollToTopButtonLabel: string; +} diff --git a/src/components/FullscreenModal/FullscreenModal.enums.ts b/src/components/FullscreenModal/FullscreenModal.enums.ts new file mode 100644 index 000000000..e8a9aec77 --- /dev/null +++ b/src/components/FullscreenModal/FullscreenModal.enums.ts @@ -0,0 +1,12 @@ +export const FullscreenModalSizes = { + lg: 'lg', + md: 'md', + sm: 'sm', +} as const; + +export const FullscreenModalLayouts = { + single6: 'single-6', + single8: 'single-8', + sidebar46: 'sidebar-4-6', + sidebar48: 'sidebar-4-8', +} as const; diff --git a/src/components/FullscreenModal/FullscreenModal.stories.tsx b/src/components/FullscreenModal/FullscreenModal.stories.tsx new file mode 100644 index 000000000..e7ba345b0 --- /dev/null +++ b/src/components/FullscreenModal/FullscreenModal.stories.tsx @@ -0,0 +1,173 @@ +import React from 'react'; +import { Meta, Story } from '@storybook/react/types-6-0'; +import { action } from '@storybook/addon-actions'; + +import { FlexContainer } from '../FlexContainer'; +import { Button } from '../Button'; +import { ButtonVariants } from '../Button/Button.enums'; +import { Link, Paragraph } from '../typography'; +import FullscreenModal from './FullscreenModal'; +import { FullscreenModalLayouts } from './FullscreenModal.enums'; + +export default { + title: 'components/FullscreenModal', + component: FullscreenModal, + parameters: { + docs: { + inlineStories: false, + iframeHeight: 500, + source: { type: 'code' }, + }, + screenshot: { + viewport: { + width: 1280, + height: 720, + }, + }, + }, +} as Meta; + +const header = 'Invite vendor to SecurityScorecard'; +const Content = () => ( + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam volutpat velit + vel urna molestie, vitae sodales sem hendrerit. Nunc risus nibh, rhoncus ut + massa id, eleifend lacinia orci. Morbi porta, urna ut tincidunt efficitur, + lorem nulla facilisis orci, sit amet rutrum augue elit ut elit. Interdum et + malesuada fames ac ante ipsum primis in faucibus. Suspendisse consectetur + lectus finibus diam posuere, elementum vehicula sapien placerat. In sed + ornare ex, quis lacinia lorem. Nunc rhoncus lorem a laoreet posuere. Nam + cursus lorem vestibulum semper pulvinar. Nunc tempus ornare urna, sit amet + varius nisl fringilla et. Fusce volutpat urna et aliquet dictum. In nec + cursus elit. Vivamus congue ac elit placerat suscipit. Nulla facilisi. + Praesent fringilla, quam sit amet blandit tempor, risus leo bibendum leo, ut + aliquet metus leo non neque. Etiam in ante arcu. + +); +const LongContent = () => ( + <> + + + + + + + +); +const Footer = () => ( + + + + + + + +); +const Sidebar = () => ( + +); + +export const SingleColumn6: Story = () => ( + +); +SingleColumn6.storyName = 'Single Column (layout: single-6)'; +SingleColumn6.argTypes = { + header: { + control: { disable: true }, + description: 'Content of the header wrapper', + table: { type: { summary: 'React.node' } }, + }, + content: { + control: { disable: true }, + description: 'Content of the content wrapper', + table: { type: { summary: 'React.node' } }, + }, + footer: { + control: { disable: true }, + description: 'Content of the footer wrapper', + table: { type: { summary: 'React.node' } }, + }, + sidebar: { + control: { disable: true }, + description: 'Content of the sidebar wrapper', + table: { type: { summary: 'React.node' } }, + }, + scrollToTopButtonLabel: { + control: { disable: true }, + description: 'Label of the scroll to top button', + table: { defaultValue: { summary: '"Scroll to top"' } }, + }, + onClose: { + control: { disable: true }, + description: 'Modal window close handler', + }, +}; + +export const SingleColumn8: Story = () => ( + +); + +SingleColumn8.storyName = 'Single Column (layout: single-8)'; + +export const Sidebar4Column6: Story = () => ( + +); +Sidebar4Column6.storyName = 'Two Columns with sidebar (layout: sidebar-4-6)'; + +export const Sidebar4Column8: Story = () => ( + +); +Sidebar4Column8.storyName = 'Two Columns with sidebar (layout: sidebar-4-8)'; + +export const WithLongContent: Story = () => ( + +); diff --git a/src/components/FullscreenModal/FullscreenModal.tsx b/src/components/FullscreenModal/FullscreenModal.tsx new file mode 100644 index 000000000..59bd50435 --- /dev/null +++ b/src/components/FullscreenModal/FullscreenModal.tsx @@ -0,0 +1,123 @@ +import React, { useRef } from 'react'; +import PropTypes from 'prop-types'; +import styled from 'styled-components'; +import { isUndefined, noop } from 'ramda-adjunct'; + +import { getColor, getDepth } from '../../utils/helpers'; +import ModalHeader from './Header/Header'; +import ModalFooter from './Footer/Footer'; +import { FullscreenModalLayouts } from './FullscreenModal.enums'; +import { ColumnConfigMap, FullscreenModalProps } from './FullscreenModal.types'; +import { Col, Container, Row } from '../layout'; + +const BaseModal = styled.div` + width: 100%; + height: 100%; + position: fixed; + top: 0; + left: 0; + overflow-y: auto; + background-color: ${getColor('graphite5H')}; + z-index: ${getDepth('modal')}; +`; + +const columnConfigMap: ColumnConfigMap = { + [FullscreenModalLayouts.single6]: { + header: [6, 3], + sidebar: [0, 0], + content: [6, 3], + }, + [FullscreenModalLayouts.single8]: { + header: [8, 2], + sidebar: [0, 0], + content: [8, 2], + }, + [FullscreenModalLayouts.sidebar46]: { + header: [10, 1], + sidebar: [4, 1], + content: [6, 0], + }, + [FullscreenModalLayouts.sidebar48]: { + header: [12, 0], + sidebar: [4, 0], + content: [8, 0], + }, +}; + +const FullscreenModal: React.FC = ({ + layout = FullscreenModalLayouts.single6, + header, + content, + sidebar, + footer, + scrollToTopButtonLabel, + onClose = noop, +}) => { + const modalRef = useRef(null); + + const { + header: [headerCols, headerOffset], + sidebar: [sidebarCols, sidebarOffset], + content: [contentCols, contentOffset], + } = columnConfigMap[layout]; + + const hasLayoutSidebar = sidebarCols > 0; + const totalContentWidth = contentCols + sidebarCols; + + if (hasLayoutSidebar && isUndefined(sidebar)) { + throw new Error( + `You chose to use modal layout with sidebar (current: ${layout}) but you didn't provide sidebar content. +You should either provide content in "sidebar" property or switch layout to "${FullscreenModalLayouts.single6}" or "${FullscreenModalLayouts.single8}" +`, + ); + } + + return ( + + + + + + {header} + + + + + {hasLayoutSidebar && ( + + {sidebar} + + )} + + {content} + + {footer} + + + + + + ); +}; + +FullscreenModal.propTypes = { + header: PropTypes.node.isRequired, + content: PropTypes.node.isRequired, + footer: PropTypes.node.isRequired, + onClose: PropTypes.func.isRequired, + sidebar: PropTypes.node, + layout: PropTypes.oneOf(Object.values(FullscreenModalLayouts)), + scrollToTopButtonLabel: PropTypes.string, +}; + +export default FullscreenModal; diff --git a/src/components/FullscreenModal/FullscreenModal.types.ts b/src/components/FullscreenModal/FullscreenModal.types.ts new file mode 100644 index 000000000..db95b395e --- /dev/null +++ b/src/components/FullscreenModal/FullscreenModal.types.ts @@ -0,0 +1,21 @@ +import { + FullscreenModalLayouts, + FullscreenModalSizes, +} from './FullscreenModal.enums'; + +export type Sizes = typeof FullscreenModalSizes[keyof typeof FullscreenModalSizes]; +export type Layouts = typeof FullscreenModalLayouts[keyof typeof FullscreenModalLayouts]; +export type ColumnConfig = Record< + 'header' | 'sidebar' | 'content', + [number, number] +>; +export type ColumnConfigMap = Record; +export interface FullscreenModalProps { + layout: Layouts; + header: React.ReactNode; + content: React.ReactNode; + footer: React.ReactNode; + sidebar?: React.ReactNode; + scrollToTopButtonLabel?: string; + onClose: () => void; +} diff --git a/src/components/FullscreenModal/Header/Header.tsx b/src/components/FullscreenModal/Header/Header.tsx new file mode 100644 index 000000000..ba34205c4 --- /dev/null +++ b/src/components/FullscreenModal/Header/Header.tsx @@ -0,0 +1,96 @@ +import React, { useRef } from 'react'; +import PropTypes from 'prop-types'; +import styled from 'styled-components'; + +import { getColor, pxToRem } from '../../../utils/helpers'; +import { Button } from '../../Button'; +import { + ButtonColors, + ButtonSizes, + ButtonVariants, +} from '../../Button/Button.enums'; +import { FlexContainer } from '../../FlexContainer'; +import { Icon } from '../../Icon'; +import { SSCIconNames } from '../../Icon/Icon.enums'; +import { Col, Container, Row } from '../../layout'; +import { H2, H3 } from '../../typography'; +import { useStickyHeader } from '../hooks/useStickyHeader'; +import { HeaderProps } from './Header.types'; + +const BaseStickyHeader = styled.header` + position: fixed; + top: 0; + left: 0; + width: 100%; + background-color: ${getColor('graphite5H')}; + border-bottom: 1px solid ${getColor('graphiteHB')}; +`; + +const BaseHeader = styled.header` + padding: ${pxToRem(56, 0, 24)}; +`; + +const Header: React.FC = ({ + children, + width, + offset, + modalRef, + handleClose, +}) => { + const modalHeaderRef = useRef(null); + const { isFixed } = useStickyHeader(modalRef, modalHeaderRef); + + return ( + <> + {isFixed && ( + + + + + +

+ {children} +

+ + +
+ +
+
+
+ )} + +

{children}

+
+ + ); +}; + +Header.propTypes = { + width: PropTypes.number.isRequired, + offset: PropTypes.number.isRequired, + modalRef: PropTypes.exact({ + // eslint-disable-next-line react/no-unused-prop-types + current: PropTypes.instanceOf(HTMLElement), + }).isRequired, + handleClose: PropTypes.func.isRequired, +}; + +export default Header; diff --git a/src/components/FullscreenModal/Header/Header.types.ts b/src/components/FullscreenModal/Header/Header.types.ts new file mode 100644 index 000000000..42c700b11 --- /dev/null +++ b/src/components/FullscreenModal/Header/Header.types.ts @@ -0,0 +1,6 @@ +export interface HeaderProps { + width: number; + offset: number; + modalRef: React.MutableRefObject; + handleClose: () => void; +} diff --git a/src/components/FullscreenModal/ScrollToTopButton/ScrollToTopButton.tsx b/src/components/FullscreenModal/ScrollToTopButton/ScrollToTopButton.tsx new file mode 100644 index 000000000..27e25e422 --- /dev/null +++ b/src/components/FullscreenModal/ScrollToTopButton/ScrollToTopButton.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import styled from 'styled-components'; +import { isNotUndefined } from 'ramda-adjunct'; + +import { pxToRem } from '../../../utils/helpers'; +import { Button } from '../../Button'; +import { ButtonColors, ButtonVariants } from '../../Button/Button.enums'; +import { Icon } from '../../Icon'; +import { SSCIconNames } from '../../Icon/Icon.enums'; +import { ScrollToTopButtonProps } from './ScrollToTopButton.types'; + +const StyledButton = styled(Button)` + flex-direction: column; + justify-content: center; + height: ${pxToRem(48)}; + padding: 0; + text-align: center; + line-height: ${pxToRem(13)}; +`; + +const ButtonText = styled.span` + margin-top: ${pxToRem(4)}; +`; + +const ScrollToTopButton: React.FC = ({ + onClick, + label = 'Scroll to top', +}) => { + const handleClick = () => { + if (isNotUndefined(onClick)) { + onClick(); + } else { + window.scrollTo(0, 0); + } + }; + + return ( + + + {label} + + ); +}; + +ScrollToTopButton.propTypes = { + label: PropTypes.string, + onClick: PropTypes.func, +}; + +export default ScrollToTopButton; diff --git a/src/components/FullscreenModal/ScrollToTopButton/ScrollToTopButton.types.ts b/src/components/FullscreenModal/ScrollToTopButton/ScrollToTopButton.types.ts new file mode 100644 index 000000000..a0ebadc15 --- /dev/null +++ b/src/components/FullscreenModal/ScrollToTopButton/ScrollToTopButton.types.ts @@ -0,0 +1,4 @@ +export interface ScrollToTopButtonProps { + onClick?: () => void; + label?: string; +} diff --git a/src/components/FullscreenModal/ScrollToTopButton/index.ts b/src/components/FullscreenModal/ScrollToTopButton/index.ts new file mode 100644 index 000000000..81bc437a7 --- /dev/null +++ b/src/components/FullscreenModal/ScrollToTopButton/index.ts @@ -0,0 +1 @@ +export { default as ScrollToTopButton } from './ScrollToTopButton'; diff --git a/src/components/FullscreenModal/hooks/useStickyFooter.ts b/src/components/FullscreenModal/hooks/useStickyFooter.ts new file mode 100644 index 000000000..2c2156b3d --- /dev/null +++ b/src/components/FullscreenModal/hooks/useStickyFooter.ts @@ -0,0 +1,59 @@ +import { useCallback, useEffect, useState } from 'react'; +import debounce from 'lodash.debounce'; +import { isNull } from 'ramda-adjunct'; + +import { useStickyObserver } from './useStickyObserver'; + +export const useStickyFooter = ( + modalRef: React.MutableRefObject, + modalFooterRef: React.MutableRefObject, +): { isFixed: boolean; shouldShowScrollToTopButton: boolean } => { + const [isFixed, setIsFixed] = useState(false); + const [ + shouldShowScrollToTopButton, + setShouldShowScrollToTopButton, + ] = useState(false); + + const showScrollToTopButton = useCallback(() => { + if (isNull(modalRef.current)) return; + const isScrollable = + modalRef.current.scrollHeight > modalRef.current.offsetHeight; + setShouldShowScrollToTopButton(isScrollable); + setIsFixed(false); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const resizeListener = debounce(showScrollToTopButton, 200); + useEffect(() => { + showScrollToTopButton(); + window.addEventListener('resize', resizeListener); + + return () => { + window.removeEventListener('resize', resizeListener); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const isInView = useCallback(() => { + if (isNull(modalRef.current) || isNull(modalFooterRef.current)) return; + const scrollOffset = + modalRef.current.scrollTop + modalRef.current.offsetHeight; + const contentHeight = + modalRef.current.scrollHeight - modalFooterRef.current.scrollHeight; + const fixedAtThreshold = contentHeight * 0.001; + + if (isFixed && scrollOffset >= contentHeight) { + setIsFixed(false); + } else if (!isFixed && scrollOffset < contentHeight - fixedAtThreshold) { + setIsFixed(true); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isFixed]); + + useStickyObserver(modalRef, modalFooterRef, isInView); + + return { + isFixed, + shouldShowScrollToTopButton, + }; +}; diff --git a/src/components/FullscreenModal/hooks/useStickyHeader.ts b/src/components/FullscreenModal/hooks/useStickyHeader.ts new file mode 100644 index 000000000..d9e03aa6f --- /dev/null +++ b/src/components/FullscreenModal/hooks/useStickyHeader.ts @@ -0,0 +1,30 @@ +import { useCallback, useState } from 'react'; +import { isNull } from 'ramda-adjunct'; + +import { useStickyObserver } from './useStickyObserver'; + +export const useStickyHeader = ( + modalRef: React.MutableRefObject, + modalHeaderRef: React.MutableRefObject, +): { isFixed: boolean } => { + const [isFixed, setIsFixed] = useState(false); + + const isInView = useCallback(() => { + if (isNull(modalRef.current) || isNull(modalHeaderRef.current)) return; + const scrollOffset = modalRef.current.scrollTop; + const headerHeight = modalHeaderRef.current.scrollHeight; + + if (isFixed && scrollOffset <= headerHeight) { + setIsFixed(false); + } else if (!isFixed && scrollOffset > headerHeight) { + setIsFixed(true); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isFixed]); + + useStickyObserver(modalRef, modalHeaderRef, isInView); + + return { + isFixed, + }; +}; diff --git a/src/components/FullscreenModal/hooks/useStickyObserver.ts b/src/components/FullscreenModal/hooks/useStickyObserver.ts new file mode 100644 index 000000000..d331b220d --- /dev/null +++ b/src/components/FullscreenModal/hooks/useStickyObserver.ts @@ -0,0 +1,59 @@ +import { useEffect, useRef } from 'react'; +import debounce from 'lodash.debounce'; +import { isNotNull } from 'ramda-adjunct'; + +export const useStickyObserver = ( + modalRef: React.MutableRefObject, + elementRef: React.MutableRefObject, + isInView: () => void, +): void => { + const observerRef = useRef(null); + const listeners = useRef(null); + const areListenersAdded = useRef(false); + + useEffect(() => { + const currentModalRef = modalRef.current; + listeners.current = { + resizeListener: debounce(isInView, 200), + scrollListener: debounce(isInView, 100), + }; + + if (areListenersAdded.current && isNotNull(listeners.current)) { + currentModalRef.removeEventListener( + 'scroll', + listeners.current.scrollListener, + ); + window.removeEventListener('resize', listeners.current.resizeListener); + } + + if (isNotNull(observerRef.current)) { + observerRef.current.disconnect(); + } + observerRef.current = new MutationObserver(() => isInView()); + + if (isNotNull(currentModalRef) && isNotNull(elementRef.current)) { + observerRef.current.observe(currentModalRef, { + childList: true, + subtree: true, + attributes: true, + }); + isInView(); + + currentModalRef.addEventListener( + 'scroll', + listeners.current.scrollListener, + ); + window.addEventListener('resize', listeners.current.resizeListener); + areListenersAdded.current = true; + } + return () => { + observerRef.current.disconnect(); + currentModalRef.removeEventListener( + 'scroll', + listeners.current.scrollListener, + ); + window.removeEventListener('resize', listeners.current.resizeListener); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isInView]); +}; diff --git a/src/components/FullscreenModal/index.ts b/src/components/FullscreenModal/index.ts new file mode 100644 index 000000000..2f7855949 --- /dev/null +++ b/src/components/FullscreenModal/index.ts @@ -0,0 +1,2 @@ +export * as FullscreenModalEnums from './FullscreenModal.enums'; +export { default as FullscreenModal } from './FullscreenModal'; diff --git a/src/components/Icon/Icon.enums.ts b/src/components/Icon/Icon.enums.ts index be2a9e713..c8720a015 100644 --- a/src/components/Icon/Icon.enums.ts +++ b/src/components/Icon/Icon.enums.ts @@ -6,6 +6,7 @@ export const SSCIconNames = { check: 'check', times: 'times', chevronDown: 'chevron-down', + arrowUp: 'arrow-up', } as const; export const IconTypes = { diff --git a/src/components/Tooltip/Tooltip.tsx b/src/components/Tooltip/Tooltip.tsx index 0f1d0724a..f3ec1d921 100644 --- a/src/components/Tooltip/Tooltip.tsx +++ b/src/components/Tooltip/Tooltip.tsx @@ -7,6 +7,7 @@ import { createPaddingSpacing, getBorderRadius, getColor, + getDepth, pxToRem, } from '../../utils/helpers'; import { Placements, TooltipProps } from './Tooltip.types'; @@ -77,7 +78,7 @@ const tooltipPlacements = { const Popup = styled.div<{ placement: Placements }>` position: absolute; - z-index: 10; + z-index: ${getDepth('tooltip')}; width: ${pxToRem(270)}; background: ${getColor('graphite5H')}; ${createPaddingSpacing(0.75)}; diff --git a/src/components/index.ts b/src/components/index.ts index 613b9f349..10afa398b 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -6,8 +6,8 @@ export * from './FlexContainer'; export * from './HexGrade'; export * from './Icon'; export * from './IconButton'; +export * from './FullscreenModal'; export * from './Nav'; -export * from './Signal'; export * from './Spinner'; export * from './Tabs'; export * from './Toast'; diff --git a/src/theme/buttons.ts b/src/theme/buttons.ts index 421765e7e..ca4f00590 100644 --- a/src/theme/buttons.ts +++ b/src/theme/buttons.ts @@ -31,6 +31,11 @@ const buttons: Buttons = { hoverColor: colors.radiantBlueberry, activeColor: colors.dietBlueberry, }, + secondary: { + color: colors.graphite2B, + hoverColor: colors.graphite4B, + activeColor: colors.graphite5B, + }, danger: { color: colors.cherry, hoverColor: colors.strawberry, diff --git a/src/theme/depths.ts b/src/theme/depths.ts new file mode 100644 index 000000000..f8f6eb485 --- /dev/null +++ b/src/theme/depths.ts @@ -0,0 +1,8 @@ +import { Depths } from './depths.types'; + +const depths: Depths = { + tooltip: 1100, + modal: 1250, +}; + +export default depths; diff --git a/src/theme/depths.types.ts b/src/theme/depths.types.ts new file mode 100644 index 000000000..d1a2e97d0 --- /dev/null +++ b/src/theme/depths.types.ts @@ -0,0 +1,4 @@ +export interface Depths { + tooltip: number; + modal: number; +} diff --git a/src/theme/icons/arrowUp.ts b/src/theme/icons/arrowUp.ts new file mode 100644 index 000000000..8cfe814df --- /dev/null +++ b/src/theme/icons/arrowUp.ts @@ -0,0 +1,17 @@ +import { + IconDefinition, + IconName, + IconPrefix, +} from '@fortawesome/fontawesome-svg-core'; + +export const width = 9; +export const height = 13; +export const unicode = 'e007'; +export const svgPathData = + 'M 3.616 1.866 L 4.324 1.159 C 4.421 1.061 4.579 1.061 4.677 1.159 L 8.053 4.534 C 8.17 4.652 8.17 4.841 8.053 4.959 L 7.593 5.418 C 7.476 5.535 7.286 5.535 7.169 5.418 L 5.125 3.375 L 5.125 11.68 C 5.125 11.846 4.991 11.98 4.825 11.98 L 4.175 11.98 C 4.009 11.98 3.875 11.846 3.875 11.68 L 3.875 3.385 L 1.831 5.428 C 1.714 5.545 1.524 5.545 1.407 5.428 L 0.947 4.968 C 0.83 4.851 0.83 4.661 0.947 4.544 L 3.621 1.871 Z'; + +export const arrowUp = { + prefix: 'ssc' as IconPrefix, + iconName: 'arrow-up' as IconName, + icon: [width, height, [], unicode, svgPathData], +} as IconDefinition; diff --git a/src/theme/icons/index.ts b/src/theme/icons/index.ts index f57aad53b..73cbae871 100644 --- a/src/theme/icons/index.ts +++ b/src/theme/icons/index.ts @@ -10,3 +10,4 @@ export { eyeSlash } from './eyeSlash'; export { check } from './check'; export { times } from './times'; export { chevronDown } from './chevronDown'; +export { arrowUp } from './arrowUp'; diff --git a/src/theme/theme.ts b/src/theme/theme.ts index eeb86af8a..daaae35ea 100644 --- a/src/theme/theme.ts +++ b/src/theme/theme.ts @@ -5,6 +5,7 @@ import forms from './forms'; import typography from './typography'; import buttons from './buttons'; import layout from './layout'; +import depths from './depths'; export const theme: DefaultTheme = { colors, @@ -12,5 +13,6 @@ export const theme: DefaultTheme = { buttons, forms, layout, + depths, borderRadius: 4, }; diff --git a/src/types/definitions/styled.d.ts b/src/types/definitions/styled.d.ts index 01c0a0670..5f479cc00 100644 --- a/src/types/definitions/styled.d.ts +++ b/src/types/definitions/styled.d.ts @@ -5,6 +5,7 @@ import { Typography } from '../../theme/typography.types'; import { Buttons } from '../../theme/buttons.types'; import { Forms } from '../../theme/forms.types'; import { Layout } from '../../theme/layout.types'; +import { Depths } from '../../theme/depths.types'; declare module 'styled-components' { export interface DefaultTheme { @@ -13,6 +14,7 @@ declare module 'styled-components' { buttons: Buttons; forms: Forms; layout: Layout; + depths: Depths; borderRadius: number; } } diff --git a/src/utils/helpers.stories.mdx b/src/utils/helpers.stories.mdx index 226963fd7..57c6b5786 100644 --- a/src/utils/helpers.stories.mdx +++ b/src/utils/helpers.stories.mdx @@ -254,4 +254,18 @@ const Link = styled.a.attrs(() => ({ color: ${getLinkStyle('activeColor')}; } `; -``` \ No newline at end of file +``` +### getDepth + +Returns z-index from theme for given element. + +```js +import { getDepth } from '@securityscorecard/design-system'; + +// getDepth :: Element -> Props -> string +// Element - any key of 'depth' (src/theme/depths.ts) +// Props - styled-components props object +const Component = styled.input` + z-index: ${getDepth('modal')}; +`; // -> z-index: 1250; +``` diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index 1f2295580..ed48227a0 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -33,6 +33,7 @@ import { import { Colors } from '../theme/colors.types'; import { Forms } from '../theme/forms.types'; import { SpacingSizeValue } from '../types/spacing.types'; +import { Depths } from '../theme/depths.types'; type Theme = { theme?: DefaultTheme; @@ -128,6 +129,14 @@ export const getLinkStyle = curry((type, { color, theme }) => { return path(['typography', 'links', 'primary', type], theme); }); +// getDepth :: Element -> Props -> string +// Element - any key of 'depth' (src/theme/depths.ts) +// Props - styled-components props object +export const getDepth = curry( + (element: keyof Depths, { theme }: Theme): string => + path(['depths', element], theme), +); + type SpacingKind = 'padding' | 'margin'; const calculateSpacingValue = (direction: number, generic: number) => diff --git a/yarn.lock b/yarn.lock index 2a97f8669..a0b1b2fbb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10657,6 +10657,11 @@ lodash.clonedeep@^4.5.0, lodash.clonedeep@~4.5.0: resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= +lodash.debounce@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" + integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168= + lodash.escaperegexp@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz#64762c48618082518ac3df4ccf5d5886dae20347"