From 8918218cdd229bd09afd228e636e135aba533f90 Mon Sep 17 00:00:00 2001 From: simeng-li Date: Fri, 15 Apr 2022 14:25:44 +0800 Subject: [PATCH] feat(ui): add Primary SocialSignIn container and Popup (#549) * feat(ui): add Primary SocialSignIn container and Popup add Primary SocialSignIn container and Popup * fix(ui): fix popup component bug fix popup component bug * feat(ui): add transition styles to Drawer add transition styles to Drawer * fix(ui): fix typo fix typo * fix(ui): fix bugs fix bugs * test(ui): ass simple test case for drawer add simeple test case for drawer * fix(ui): cr fix --- packages/ui/src/__mocks__/logto.tsx | 20 +++++-- packages/ui/src/assets/icons/expand-icon.svg | 4 ++ .../ui/src/components/ConfirmModal/index.tsx | 8 +-- .../src/components/Drawer/index.module.scss | 13 +++++ .../ui/src/components/Drawer/index.test.tsx | 16 ++++++ packages/ui/src/components/Drawer/index.tsx | 40 ++++++++++++++ .../src/components/Icons/ExpandMoreIcon.tsx | 11 ++++ packages/ui/src/components/Icons/index.ts | 3 + .../ui/src/components/TermsOfUse/index.tsx | 2 +- .../SocialSignIn/PrimarySocialSignIn.test.tsx | 36 ++++++++++++ .../SocialSignIn/PrimarySocialSignIn.tsx | 55 +++++++++++++++++++ .../SocialSignIn/SecondarySocialSignIn.tsx | 17 +++--- .../SocialSignIn/SocialSignInPopUp.tsx | 21 +++++++ .../containers/SocialSignIn/index.module.scss | 21 +++++-- .../ui/src/containers/SocialSignIn/index.ts | 2 + packages/ui/src/pages/Passcode/index.tsx | 2 +- packages/ui/src/pages/Register/index.tsx | 2 +- .../ui/src/pages/SecondarySignIn/index.tsx | 2 +- packages/ui/src/scss/modal.module.scss | 30 ++++++++++ 19 files changed, 278 insertions(+), 27 deletions(-) create mode 100644 packages/ui/src/assets/icons/expand-icon.svg create mode 100644 packages/ui/src/components/Drawer/index.module.scss create mode 100644 packages/ui/src/components/Drawer/index.test.tsx create mode 100644 packages/ui/src/components/Drawer/index.tsx create mode 100644 packages/ui/src/components/Icons/ExpandMoreIcon.tsx create mode 100644 packages/ui/src/containers/SocialSignIn/PrimarySocialSignIn.test.tsx create mode 100644 packages/ui/src/containers/SocialSignIn/PrimarySocialSignIn.tsx create mode 100644 packages/ui/src/containers/SocialSignIn/SocialSignInPopUp.tsx diff --git a/packages/ui/src/__mocks__/logto.tsx b/packages/ui/src/__mocks__/logto.tsx index 7abed3694..30360b666 100644 --- a/packages/ui/src/__mocks__/logto.tsx +++ b/packages/ui/src/__mocks__/logto.tsx @@ -4,27 +4,37 @@ export const socialConnectors = [ { id: 'github', logo: 'https://user-images.githubusercontent.com/5717882/156983224-7ea0296b-38fa-419d-9515-67e8a9612e09.png', - name: 'GitHub', + name: { + en: 'Sign in with GitHub', + }, }, { id: 'alipay', logo: 'https://user-images.githubusercontent.com/5717882/156983224-7ea0296b-38fa-419d-9515-67e8a9612e09.png', - name: 'alipay', + name: { + en: 'Sign in with Alipay', + }, }, { id: 'wechat', logo: 'https://user-images.githubusercontent.com/5717882/156983224-7ea0296b-38fa-419d-9515-67e8a9612e09.png', - name: 'wechat', + name: { + en: 'Sign in with WeChat', + }, }, { id: 'google', logo: 'https://user-images.githubusercontent.com/5717882/156983224-7ea0296b-38fa-419d-9515-67e8a9612e09.png', - name: 'google', + name: { + en: 'Sign in with Google', + }, }, { id: 'facebook', logo: 'https://user-images.githubusercontent.com/5717882/156983224-7ea0296b-38fa-419d-9515-67e8a9612e09.png', - name: 'Meta', + name: { + en: 'Sign in with Meta', + }, }, ]; diff --git a/packages/ui/src/assets/icons/expand-icon.svg b/packages/ui/src/assets/icons/expand-icon.svg new file mode 100644 index 000000000..fc37b6d93 --- /dev/null +++ b/packages/ui/src/assets/icons/expand-icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/ui/src/components/ConfirmModal/index.tsx b/packages/ui/src/components/ConfirmModal/index.tsx index 39d6ab8cb..79ba734ca 100644 --- a/packages/ui/src/components/ConfirmModal/index.tsx +++ b/packages/ui/src/components/ConfirmModal/index.tsx @@ -4,12 +4,12 @@ import { TFuncKey, useTranslation } from 'react-i18next'; import ReactModal from 'react-modal'; import Button from '@/components/Button'; -import * as modalStyles from '@/scss/modal.module.scss'; +import * as modalStyles from '../../scss/modal.module.scss'; import * as styles from './index.module.scss'; type Props = { - classname?: string; + className?: string; isOpen?: boolean; children: ReactNode; cancelText?: TFuncKey<'translation', 'main_flow'>; @@ -19,7 +19,7 @@ type Props = { }; const ConfirmModal = ({ - classname, + className, isOpen = false, children, cancelText = 'action.cancel', @@ -33,7 +33,7 @@ const ConfirmModal = ({ document.querySelector('main') ?? document.body} > diff --git a/packages/ui/src/components/Drawer/index.module.scss b/packages/ui/src/components/Drawer/index.module.scss new file mode 100644 index 000000000..484639e66 --- /dev/null +++ b/packages/ui/src/components/Drawer/index.module.scss @@ -0,0 +1,13 @@ +@use '@/scss/underscore' as _; + +.container { + padding: _.unit(5); + background: var(--color-background); +} + +.header { + @include _.flex-row; + justify-content: flex-end; + align-items: center; + margin-bottom: _.unit(4); +} diff --git a/packages/ui/src/components/Drawer/index.test.tsx b/packages/ui/src/components/Drawer/index.test.tsx new file mode 100644 index 000000000..a98871530 --- /dev/null +++ b/packages/ui/src/components/Drawer/index.test.tsx @@ -0,0 +1,16 @@ +import { render } from '@testing-library/react'; +import React from 'react'; + +import Drawer from '.'; + +describe('Drawer', () => { + it('render children', () => { + const { queryByText } = render( + + children + + ); + + expect(queryByText('children')).not.toBeNull(); + }); +}); diff --git a/packages/ui/src/components/Drawer/index.tsx b/packages/ui/src/components/Drawer/index.tsx new file mode 100644 index 000000000..e50c1cd90 --- /dev/null +++ b/packages/ui/src/components/Drawer/index.tsx @@ -0,0 +1,40 @@ +import classNames from 'classnames'; +import React, { ReactNode } from 'react'; +import ReactModal from 'react-modal'; + +import { ClearIcon } from '@/components/Icons'; + +import * as modalStyles from '../../scss/modal.module.scss'; +import * as styles from './index.module.scss'; + +type Props = { + className?: string; + isOpen?: boolean; + children: ReactNode; + onClose: () => void; +}; + +const Drawer = ({ className, isOpen = false, children, onClose }: Props) => { + return ( + document.querySelector('main') ?? document.body} + appElement={document.querySelector('main') ?? document.body} + closeTimeoutMS={300} + onRequestClose={onClose} + > +
+
+ +
+ {children} +
+
+ ); +}; + +export default Drawer; diff --git a/packages/ui/src/components/Icons/ExpandMoreIcon.tsx b/packages/ui/src/components/Icons/ExpandMoreIcon.tsx new file mode 100644 index 000000000..df3092ee5 --- /dev/null +++ b/packages/ui/src/components/Icons/ExpandMoreIcon.tsx @@ -0,0 +1,11 @@ +import React, { SVGProps } from 'react'; + +import ExpandMore from '@/assets/icons/expand-icon.svg'; + +const ExpandMoreIcon = (props: SVGProps) => ( + + + +); + +export default ExpandMoreIcon; diff --git a/packages/ui/src/components/Icons/index.ts b/packages/ui/src/components/Icons/index.ts index fcb21092c..f0f2a77f5 100644 --- a/packages/ui/src/components/Icons/index.ts +++ b/packages/ui/src/components/Icons/index.ts @@ -2,3 +2,6 @@ export { default as ClearIcon } from './ClearIcon'; export { default as PrivacyIcon } from './PrivacyIcon'; export { default as DownArrowIcon } from './DownArrowIcon'; export { default as LoadingIcon } from './LoadingIcon'; +export { default as NavArrowIcon } from './NavArrowIcon'; +export { default as RadioButtonIcon } from './RadioButtonIcon'; +export { default as ExpandMoreIcon } from './ExpandMoreIcon'; diff --git a/packages/ui/src/components/TermsOfUse/index.tsx b/packages/ui/src/components/TermsOfUse/index.tsx index ae198d9c5..cbb539eaf 100644 --- a/packages/ui/src/components/TermsOfUse/index.tsx +++ b/packages/ui/src/components/TermsOfUse/index.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import ErrorMessage, { ErrorType } from '@/components/ErrorMessage'; -import RadioButtonIcon from '@/components/Icons/RadioButtonIcon'; +import { RadioButtonIcon } from '@/components/Icons'; import TextLink from '@/components/TextLink'; import * as styles from './index.module.scss'; diff --git a/packages/ui/src/containers/SocialSignIn/PrimarySocialSignIn.test.tsx b/packages/ui/src/containers/SocialSignIn/PrimarySocialSignIn.test.tsx new file mode 100644 index 000000000..49bfabef8 --- /dev/null +++ b/packages/ui/src/containers/SocialSignIn/PrimarySocialSignIn.test.tsx @@ -0,0 +1,36 @@ +import { render, fireEvent } from '@testing-library/react'; +import React from 'react'; +import { MemoryRouter } from 'react-router-dom'; + +import { socialConnectors } from '@/__mocks__/logto'; + +import PrimarySocialSignIn from './PrimarySocialSignIn'; + +describe('SecondarySocialSignIn', () => { + it('less than three connectors', () => { + const { container } = render( + + + + ); + expect(container.querySelectorAll('button')).toHaveLength(3); + }); + + it('more than three connectors', () => { + const { container } = render( + + + + ); + + expect(container.querySelectorAll('button')).toHaveLength(3); + + const expandButton = container.querySelector('svg'); + + if (expandButton) { + fireEvent.click(expandButton); + } + + expect(container.querySelectorAll('button')).toHaveLength(socialConnectors.length); + }); +}); diff --git a/packages/ui/src/containers/SocialSignIn/PrimarySocialSignIn.tsx b/packages/ui/src/containers/SocialSignIn/PrimarySocialSignIn.tsx new file mode 100644 index 000000000..162fb44b5 --- /dev/null +++ b/packages/ui/src/containers/SocialSignIn/PrimarySocialSignIn.tsx @@ -0,0 +1,55 @@ +import { ConnectorMetadata } from '@logto/schemas'; +import classNames from 'classnames'; +import React, { useState, useMemo } from 'react'; + +import SocialLinkButton from '@/components/Button/SocialLinkButton'; +import { ExpandMoreIcon } from '@/components/Icons'; +import useSocial from '@/hooks/use-social-connector'; + +import * as styles from './index.module.scss'; + +type Props = { + className?: string; + connectors: Array>; + isPopup?: boolean; +}; + +const PrimarySocialSignIn = ({ className, connectors, isPopup = false }: Props) => { + const [showAll, setShowAll] = useState(false); + const { invokeSocialSignIn } = useSocial(); + const isOverSize = connectors.length > 3; + const displayAll = showAll || isPopup || !isOverSize; + + const displayConnectors = useMemo(() => { + if (displayAll) { + return connectors; + } + + return connectors.slice(0, 3); + }, [connectors, displayAll]); + + return ( +
+ {displayConnectors.map((connector) => ( + { + void invokeSocialSignIn(connector.id); + }} + /> + ))} + {!displayAll && ( + { + setShowAll(true); + }} + /> + )} +
+ ); +}; + +export default PrimarySocialSignIn; diff --git a/packages/ui/src/containers/SocialSignIn/SecondarySocialSignIn.tsx b/packages/ui/src/containers/SocialSignIn/SecondarySocialSignIn.tsx index 131cfefa9..90fb190ed 100644 --- a/packages/ui/src/containers/SocialSignIn/SecondarySocialSignIn.tsx +++ b/packages/ui/src/containers/SocialSignIn/SecondarySocialSignIn.tsx @@ -11,25 +11,24 @@ import * as styles from './index.module.scss'; type Props = { className?: string; connectors: Array>; + showMoreConnectors?: () => void; }; -const SecondarySocialSignIn = ({ className, connectors }: Props) => { +const SecondarySocialSignIn = ({ className, connectors, showMoreConnectors }: Props) => { const { invokeSocialSignIn } = useSocial(); - const sampled = connectors.length > 4; + const isOverSize = connectors.length > 4; - const sampledConnectors = useMemo(() => { - // TODO: filter with native returned - - if (sampled) { + const displayConnectors = useMemo(() => { + if (isOverSize) { return connectors.slice(0, 3); } return connectors; - }, [connectors, sampled]); + }, [connectors, isOverSize]); return (
- {sampledConnectors.map((connector) => ( + {displayConnectors.map((connector) => ( { }} /> ))} - {sampled && } + {isOverSize && }
); }; diff --git a/packages/ui/src/containers/SocialSignIn/SocialSignInPopUp.tsx b/packages/ui/src/containers/SocialSignIn/SocialSignInPopUp.tsx new file mode 100644 index 000000000..15581bd08 --- /dev/null +++ b/packages/ui/src/containers/SocialSignIn/SocialSignInPopUp.tsx @@ -0,0 +1,21 @@ +import { ConnectorMetadata } from '@logto/schemas'; +import React from 'react'; + +import Drawer from '@/components/Drawer'; + +import PrimarySocialSignIn from './PrimarySocialSignIn'; + +type Props = { + isOpen?: boolean; + onClose: () => void; + className?: string; + connectors: Array>; +}; + +const SocialSignInPopUp = ({ isOpen = false, onClose, className, connectors }: Props) => ( + + + +); + +export default SocialSignInPopUp; diff --git a/packages/ui/src/containers/SocialSignIn/index.module.scss b/packages/ui/src/containers/SocialSignIn/index.module.scss index 8e46bbb57..5abf32297 100644 --- a/packages/ui/src/containers/SocialSignIn/index.module.scss +++ b/packages/ui/src/containers/SocialSignIn/index.module.scss @@ -5,12 +5,23 @@ max-width: 360px; @include _.flex-row; justify-content: center; -} -.socialButton { - margin-right: _.unit(10); + .socialButton { + margin-right: _.unit(10); - &:last-child { - margin-right: 0; + &:last-child { + margin-right: 0; + } + } +} + +.socialLinkList { + width: 100%; + max-width: 360px; + @include _.flex-column; + margin: 0 auto; + + .socialLinkButton { + margin-bottom: _.unit(4); } } diff --git a/packages/ui/src/containers/SocialSignIn/index.ts b/packages/ui/src/containers/SocialSignIn/index.ts index a5247871b..ae3d182ac 100644 --- a/packages/ui/src/containers/SocialSignIn/index.ts +++ b/packages/ui/src/containers/SocialSignIn/index.ts @@ -1 +1,3 @@ export { default as SecondarySocialSignIn } from './SecondarySocialSignIn'; +export { default as PrimarySocialSignIn } from './PrimarySocialSignIn'; +export { default as SocialSignInPopUp } from './SocialSignInPopUp'; diff --git a/packages/ui/src/pages/Passcode/index.tsx b/packages/ui/src/pages/Passcode/index.tsx index 2d0baeabd..90a87d388 100644 --- a/packages/ui/src/pages/Passcode/index.tsx +++ b/packages/ui/src/pages/Passcode/index.tsx @@ -3,7 +3,7 @@ import React, { useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate, useParams, useLocation } from 'react-router-dom'; -import NavArrowIcon from '@/components/Icons/NavArrowIcon'; +import { NavArrowIcon } from '@/components/Icons'; import PasscodeValidation from '@/containers/PasscodeValidation'; import { UserFlow } from '@/types'; diff --git a/packages/ui/src/pages/Register/index.tsx b/packages/ui/src/pages/Register/index.tsx index 7fc35fed7..b656826ca 100644 --- a/packages/ui/src/pages/Register/index.tsx +++ b/packages/ui/src/pages/Register/index.tsx @@ -2,7 +2,7 @@ import React, { useMemo, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate, useParams } from 'react-router-dom'; -import NavArrowIcon from '@/components/Icons/NavArrowIcon'; +import { NavArrowIcon } from '@/components/Icons'; import CreateAccount from '@/containers/CreateAccount'; import { PhonePasswordless, EmailPasswordless } from '@/containers/Passwordless'; diff --git a/packages/ui/src/pages/SecondarySignIn/index.tsx b/packages/ui/src/pages/SecondarySignIn/index.tsx index 39d3ebdd1..686875078 100644 --- a/packages/ui/src/pages/SecondarySignIn/index.tsx +++ b/packages/ui/src/pages/SecondarySignIn/index.tsx @@ -2,7 +2,7 @@ import React, { useMemo, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate, useParams } from 'react-router-dom'; -import NavArrowIcon from '@/components/Icons/NavArrowIcon'; +import { NavArrowIcon } from '@/components/Icons'; import { PhonePasswordless, EmailPasswordless } from '@/containers/Passwordless'; import UsernameSignin from '@/containers/UsernameSignin'; diff --git a/packages/ui/src/scss/modal.module.scss b/packages/ui/src/scss/modal.module.scss index 97f10855c..75322827c 100644 --- a/packages/ui/src/scss/modal.module.scss +++ b/packages/ui/src/scss/modal.module.scss @@ -7,8 +7,38 @@ outline: none; } +.drawer { + position: fixed; + left: 0; + right: 0; + bottom: 0; + max-height: 411px; + outline: none; + padding-bottom: env(safe-area-inset-bottom); +} + .overlay { position: fixed; background: rgba(0, 0, 0, 16%); inset: 0; } + + +// React modal animation +/* stylelint-disable-next-line selector-pseudo-class-no-unknown */ +:global { + .ReactModal__Content[role='popup'] { + transform: translateY(100%); + transition: transform 0.3 ease-in-out; + } + + /* stylelint-disable selector-class-pattern */ + .ReactModal__Content--after-open[role='popup'] { + transform: translateY(0); + } + + .ReactModal__Content--before-close[role='popup'] { + transform: translateY(100%); + } + /* stylelint-enable selector-class-pattern */ +}