0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-02-24 22:05:56 -05:00

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
This commit is contained in:
simeng-li 2022-04-15 14:25:44 +08:00 committed by GitHub
parent 5a88f45c42
commit 8918218cdd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 278 additions and 27 deletions

View file

@ -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',
},
},
];

View file

@ -0,0 +1,4 @@
<svg id="more" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.9507 10.293C16.5602 9.90248 15.927 9.90248 15.5365 10.293L12.001 13.8285L8.46544 10.293C8.07492 9.90248 7.44175 9.90248 7.05123 10.293C6.6607 10.6835 6.6607 11.3167 7.05123 11.7072L11.2939 15.9499C11.6844 16.3404 12.3176 16.3404 12.7081 15.9499L16.9507 11.7072C17.3412 11.3167 17.3412 10.6835 16.9507 10.293Z" fill="#AEAEAE"/>
<circle cx="12" cy="12" r="11" stroke="#AEAEAE" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 564 B

View file

@ -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 = ({
<ReactModal
role="dialog"
isOpen={isOpen}
className={classNames(modalStyles.modal, classname)}
className={classNames(modalStyles.modal, className)}
overlayClassName={modalStyles.overlay}
parentSelector={() => document.querySelector('main') ?? document.body}
>

View file

@ -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);
}

View file

@ -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(
<Drawer isOpen onClose={jest.fn()}>
children
</Drawer>
);
expect(queryByText('children')).not.toBeNull();
});
});

View file

@ -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 (
<ReactModal
shouldCloseOnOverlayClick
role="popup"
isOpen={isOpen}
className={classNames(modalStyles.drawer, className)}
overlayClassName={modalStyles.overlay}
parentSelector={() => document.querySelector('main') ?? document.body}
appElement={document.querySelector('main') ?? document.body}
closeTimeoutMS={300}
onRequestClose={onClose}
>
<div className={styles.container}>
<div className={styles.header}>
<ClearIcon onClick={onClose} />
</div>
{children}
</div>
</ReactModal>
);
};
export default Drawer;

View file

@ -0,0 +1,11 @@
import React, { SVGProps } from 'react';
import ExpandMore from '@/assets/icons/expand-icon.svg';
const ExpandMoreIcon = (props: SVGProps<SVGSVGElement>) => (
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" {...props}>
<use href={`${ExpandMore}#more`} />
</svg>
);
export default ExpandMoreIcon;

View file

@ -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';

View file

@ -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';

View file

@ -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(
<MemoryRouter>
<PrimarySocialSignIn connectors={socialConnectors.slice(0, 3)} />
</MemoryRouter>
);
expect(container.querySelectorAll('button')).toHaveLength(3);
});
it('more than three connectors', () => {
const { container } = render(
<MemoryRouter>
<PrimarySocialSignIn connectors={socialConnectors} />
</MemoryRouter>
);
expect(container.querySelectorAll('button')).toHaveLength(3);
const expandButton = container.querySelector('svg');
if (expandButton) {
fireEvent.click(expandButton);
}
expect(container.querySelectorAll('button')).toHaveLength(socialConnectors.length);
});
});

View file

@ -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<Pick<ConnectorMetadata, 'id' | 'logo' | 'name'>>;
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 (
<div className={classNames(styles.socialLinkList, className)}>
{displayConnectors.map((connector) => (
<SocialLinkButton
key={connector.id}
className={styles.socialLinkButton}
connector={connector}
onClick={() => {
void invokeSocialSignIn(connector.id);
}}
/>
))}
{!displayAll && (
<ExpandMoreIcon
className={styles.expandButton}
onClick={() => {
setShowAll(true);
}}
/>
)}
</div>
);
};
export default PrimarySocialSignIn;

View file

@ -11,25 +11,24 @@ import * as styles from './index.module.scss';
type Props = {
className?: string;
connectors: Array<Pick<ConnectorMetadata, 'id' | 'logo'>>;
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 (
<div className={classNames(styles.socialIconList, className)}>
{sampledConnectors.map((connector) => (
{displayConnectors.map((connector) => (
<SocialIconButton
key={connector.id}
className={styles.socialButton}
@ -39,7 +38,7 @@ const SecondarySocialSignIn = ({ className, connectors }: Props) => {
}}
/>
))}
{sampled && <MoreButton className={styles.socialButton} />}
{isOverSize && <MoreButton className={styles.socialButton} onClick={showMoreConnectors} />}
</div>
);
};

View file

@ -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<Pick<ConnectorMetadata, 'id' | 'logo' | 'name'>>;
};
const SocialSignInPopUp = ({ isOpen = false, onClose, className, connectors }: Props) => (
<Drawer className={className} isOpen={isOpen} onClose={onClose}>
<PrimarySocialSignIn isPopup connectors={connectors} />
</Drawer>
);
export default SocialSignInPopUp;

View file

@ -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);
}
}

View file

@ -1 +1,3 @@
export { default as SecondarySocialSignIn } from './SecondarySocialSignIn';
export { default as PrimarySocialSignIn } from './PrimarySocialSignIn';
export { default as SocialSignInPopUp } from './SocialSignInPopUp';

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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 */
}