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:
parent
5a88f45c42
commit
8918218cdd
19 changed files with 278 additions and 27 deletions
|
@ -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',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
|
|
4
packages/ui/src/assets/icons/expand-icon.svg
Normal file
4
packages/ui/src/assets/icons/expand-icon.svg
Normal 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 |
|
@ -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}
|
||||
>
|
||||
|
|
13
packages/ui/src/components/Drawer/index.module.scss
Normal file
13
packages/ui/src/components/Drawer/index.module.scss
Normal 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);
|
||||
}
|
16
packages/ui/src/components/Drawer/index.test.tsx
Normal file
16
packages/ui/src/components/Drawer/index.test.tsx
Normal 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();
|
||||
});
|
||||
});
|
40
packages/ui/src/components/Drawer/index.tsx
Normal file
40
packages/ui/src/components/Drawer/index.tsx
Normal 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;
|
11
packages/ui/src/components/Icons/ExpandMoreIcon.tsx
Normal file
11
packages/ui/src/components/Icons/ExpandMoreIcon.tsx
Normal 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;
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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;
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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;
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1 +1,3 @@
|
|||
export { default as SecondarySocialSignIn } from './SecondarySocialSignIn';
|
||||
export { default as PrimarySocialSignIn } from './PrimarySocialSignIn';
|
||||
export { default as SocialSignInPopUp } from './SocialSignInPopUp';
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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 */
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue