mirror of
https://github.com/logto-io/logto.git
synced 2025-03-31 22:51:25 -05:00
feat(ui): add social dropdown list for desktop (#834)
* feat(ui): add social dropdown list for desktop add social dropdown list for desktop * fix(ui): cr fix cr fix * fix(ui): remove useless code remove useless code
This commit is contained in:
parent
be8b8628ba
commit
36922b343f
14 changed files with 255 additions and 27 deletions
16
packages/ui/src/components/Dropdown/DropdownItem.module.scss
Normal file
16
packages/ui/src/components/Dropdown/DropdownItem.module.scss
Normal file
|
@ -0,0 +1,16 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.item {
|
||||
padding: _.unit(1.5) _.unit(2);
|
||||
border-radius: var(--radius);
|
||||
list-style: none;
|
||||
font: var(--font-body);
|
||||
color: var(--color-text);
|
||||
cursor: pointer;
|
||||
@include _.flex-row;
|
||||
overflow: hidden;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-hover);
|
||||
}
|
||||
}
|
18
packages/ui/src/components/Dropdown/DropdownItem.tsx
Normal file
18
packages/ui/src/components/Dropdown/DropdownItem.tsx
Normal file
|
@ -0,0 +1,18 @@
|
|||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
|
||||
import * as styles from './DropdownItem.module.scss';
|
||||
|
||||
type Props = {
|
||||
onClick?: () => void;
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
const DropdownItem = ({ onClick, className, children }: Props) => (
|
||||
<li className={classNames(styles.item, className)} onClick={onClick}>
|
||||
{children}
|
||||
</li>
|
||||
);
|
||||
|
||||
export default DropdownItem;
|
26
packages/ui/src/components/Dropdown/index.module.scss
Normal file
26
packages/ui/src/components/Dropdown/index.module.scss
Normal file
|
@ -0,0 +1,26 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.content {
|
||||
background: var(--color-base);
|
||||
box-shadow: var(--shadow);
|
||||
border-radius: var(--radius);
|
||||
|
||||
&.onTop {
|
||||
box-shadow: var(--shadow-reversed);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.overlay {
|
||||
background: transparent;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
.list {
|
||||
margin: 0;
|
||||
padding: _.unit(1.5) _.unit(1);
|
||||
}
|
30
packages/ui/src/components/Dropdown/index.tsx
Normal file
30
packages/ui/src/components/Dropdown/index.tsx
Normal file
|
@ -0,0 +1,30 @@
|
|||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import ReactModal, { Props as ModalProps } from 'react-modal';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
export { default as DropdownItem } from './DropdownItem';
|
||||
|
||||
type Props = ModalProps & {
|
||||
onClose?: () => void;
|
||||
};
|
||||
|
||||
const Dropdown = ({ onClose, children, className, ...rest }: Props) => {
|
||||
return (
|
||||
<ReactModal
|
||||
shouldCloseOnOverlayClick
|
||||
className={classNames(styles.content, className)}
|
||||
overlayClassName={styles.overlay}
|
||||
ariaHideApp={false}
|
||||
onRequestClose={onClose}
|
||||
{...rest}
|
||||
>
|
||||
<ul className={styles.list} onClick={onClose}>
|
||||
{children}
|
||||
</ul>
|
||||
</ReactModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dropdown;
|
|
@ -1,11 +1,18 @@
|
|||
import React, { SVGProps } from 'react';
|
||||
import React, { SVGProps, forwardRef, Ref } from 'react';
|
||||
|
||||
import More from '@/assets/icons/more-social-icon.svg';
|
||||
|
||||
const MoreSocialIcon = (props: SVGProps<SVGSVGElement>) => (
|
||||
<svg width="48" height="48" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
const MoreSocialIcon = (props: SVGProps<SVGSVGElement>, reference?: Ref<SVGSVGElement>) => (
|
||||
<svg
|
||||
width="48"
|
||||
height="48"
|
||||
viewBox="0 0 48 48"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
ref={reference}
|
||||
>
|
||||
<use href={`${More}#more`} />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default MoreSocialIcon;
|
||||
export default forwardRef(MoreSocialIcon);
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.socialLinkList {
|
||||
@include _.flex-column;
|
||||
}
|
||||
|
||||
.socialLinkButton {
|
||||
margin-bottom: _.unit(4);
|
||||
}
|
||||
|
||||
.expandIcon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
|
||||
&.expanded {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
|
@ -5,7 +5,7 @@ import SocialLinkButton from '@/components/Button/SocialLinkButton';
|
|||
import { ExpandMoreIcon } from '@/components/Icons';
|
||||
import useSocial from '@/hooks/use-social';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
import * as styles from './PrimarySocialSignIn.module.scss';
|
||||
|
||||
export const defaultSize = 3;
|
||||
|
||||
|
|
|
@ -17,23 +17,6 @@
|
|||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.socialLinkList {
|
||||
@include _.flex-column;
|
||||
}
|
||||
|
||||
.socialLinkButton {
|
||||
margin-bottom: _.unit(4);
|
||||
}
|
||||
|
||||
.expandIcon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
|
||||
&.expanded {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
:global(body.mobile) {
|
||||
.moreButton {
|
||||
width: 48px;
|
|
@ -1,12 +1,14 @@
|
|||
import classNames from 'classnames';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import React, { useMemo, useState, useRef } from 'react';
|
||||
import { isMobile } from 'react-device-detect';
|
||||
|
||||
import SocialIconButton from '@/components/Button/SocialIconButton';
|
||||
import MoreSocialIcon from '@/components/Icons/MoreSocialIcon';
|
||||
import useSocial from '@/hooks/use-social';
|
||||
|
||||
import * as styles from './SecondarySocialSignIn.module.scss';
|
||||
import SocialSignInDropdown from './SocialSignInDropdown';
|
||||
import SocialSignInPopUp from './SocialSignInPopUp';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
export const defaultSize = 4;
|
||||
|
||||
|
@ -18,6 +20,7 @@ const SecondarySocialSignIn = ({ className }: Props) => {
|
|||
const { socialConnectors, invokeSocialSignIn } = useSocial();
|
||||
const isOverSize = socialConnectors.length > defaultSize;
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const moreButtonRef = useRef<SVGSVGElement>(null);
|
||||
|
||||
const displayConnectors = useMemo(() => {
|
||||
if (isOverSize) {
|
||||
|
@ -42,6 +45,7 @@ const SecondarySocialSignIn = ({ className }: Props) => {
|
|||
))}
|
||||
{isOverSize && (
|
||||
<MoreSocialIcon
|
||||
ref={moreButtonRef}
|
||||
className={styles.moreButton}
|
||||
onClick={() => {
|
||||
setShowModal(true);
|
||||
|
@ -49,7 +53,7 @@ const SecondarySocialSignIn = ({ className }: Props) => {
|
|||
/>
|
||||
)}
|
||||
</div>
|
||||
{isOverSize && (
|
||||
{isOverSize && isMobile && (
|
||||
<SocialSignInPopUp
|
||||
isOpen={showModal}
|
||||
onClose={() => {
|
||||
|
@ -57,6 +61,16 @@ const SecondarySocialSignIn = ({ className }: Props) => {
|
|||
}}
|
||||
/>
|
||||
)}
|
||||
{isOverSize && !isMobile && (
|
||||
<SocialSignInDropdown
|
||||
anchorRef={moreButtonRef}
|
||||
isOpen={showModal}
|
||||
connectors={socialConnectors.slice(defaultSize - 1)}
|
||||
onClose={() => {
|
||||
setShowModal(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.socialDropDown {
|
||||
position: absolute;
|
||||
min-width: 208px;
|
||||
transform: translateY(-100%) scale(0);
|
||||
transform-origin: 12px bottom;
|
||||
opacity: 0%;
|
||||
transition: transform 0.1s, opacity 0.1s;
|
||||
}
|
||||
|
||||
.socialLogo {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-right: _.unit(4);
|
||||
}
|
||||
|
||||
/* stylelint-disable selector-class-pattern */
|
||||
:global(.ReactModal__Content--after-open) {
|
||||
&.socialDropDown {
|
||||
transform: translateY(-100%) scale(1);
|
||||
opacity: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.ReactModal__Content--before-close) {
|
||||
&.socialDropDown {
|
||||
transform: translateY(-100%) scale(0);
|
||||
opacity: 0%;
|
||||
}
|
||||
}
|
||||
/* stylelint-enable selector-class-pattern */
|
|
@ -0,0 +1,76 @@
|
|||
import { Language } from '@logto/phrases';
|
||||
import React, { useMemo, useState, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import Dropdown, { DropdownItem } from '@/components/Dropdown';
|
||||
import useSocial from '@/hooks/use-social';
|
||||
import { ConnectorData } from '@/types';
|
||||
|
||||
import * as styles from './SocialSignInDropdown.module.scss';
|
||||
|
||||
type Props = {
|
||||
anchorRef?: React.RefObject<HTMLElement | SVGSVGElement>;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
connectors: ConnectorData[];
|
||||
};
|
||||
|
||||
const SocialSignInDropdown = ({ isOpen, onClose, connectors, anchorRef }: Props) => {
|
||||
const {
|
||||
i18n: { language },
|
||||
} = useTranslation();
|
||||
|
||||
const { invokeSocialSignIn } = useSocial();
|
||||
|
||||
const [contentStyle, setContentStyle] = useState<{ top?: number; left?: number }>();
|
||||
|
||||
const items = useMemo(
|
||||
() =>
|
||||
connectors.map(({ id, name, logo }) => {
|
||||
const languageKey = Object.keys(name).find((key) => key === language) ?? 'en';
|
||||
const localName = name[languageKey as Language];
|
||||
|
||||
return (
|
||||
<DropdownItem
|
||||
key={id}
|
||||
onClick={() => {
|
||||
void invokeSocialSignIn(id, onClose);
|
||||
}}
|
||||
>
|
||||
<img src={logo} alt={id} className={styles.socialLogo} />
|
||||
<span>{localName}</span>
|
||||
</DropdownItem>
|
||||
);
|
||||
}),
|
||||
[connectors, language, invokeSocialSignIn, onClose]
|
||||
);
|
||||
|
||||
const adjustPosition = useCallback(() => {
|
||||
if (anchorRef?.current) {
|
||||
const { left, top } = anchorRef.current.getBoundingClientRect();
|
||||
|
||||
setContentStyle({
|
||||
left,
|
||||
top: top - 8,
|
||||
});
|
||||
}
|
||||
}, [anchorRef]);
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
isOpen={isOpen}
|
||||
className={styles.socialDropDown}
|
||||
style={{ content: contentStyle }}
|
||||
closeTimeoutMS={100}
|
||||
onClose={onClose}
|
||||
onAfterOpen={adjustPosition}
|
||||
onAfterClose={() => {
|
||||
setContentStyle(undefined);
|
||||
}}
|
||||
>
|
||||
{items}
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
export default SocialSignInDropdown;
|
|
@ -1,3 +1,2 @@
|
|||
export { default as SecondarySocialSignIn } from './SecondarySocialSignIn';
|
||||
export { default as PrimarySocialSignIn } from './PrimarySocialSignIn';
|
||||
export { default as SocialSignInPopUp } from './SocialSignInPopUp';
|
||||
|
|
|
@ -32,6 +32,11 @@ $font-family: -apple-system,
|
|||
--color-dialogue: #fff;
|
||||
--color-divider: #e0e3e3;
|
||||
--color-error: #ba1b1b;
|
||||
|
||||
// shadows
|
||||
--shadow: 0 4px 12px rgba(66, 41, 159, 12%);
|
||||
--shadow-reversed: 0 -4px 12px rgba(66, 41, 159, 12%);
|
||||
|
||||
// legacy below
|
||||
--color-toast: rgba(25, 28, 29, 80%);
|
||||
--color-overlay: rgba(25, 28, 29, 16%);
|
||||
|
@ -61,6 +66,10 @@ $font-family: -apple-system,
|
|||
--color-divider: #444748;
|
||||
--color-error: #dd3730;
|
||||
|
||||
// shadows
|
||||
--shadow: 0 4px 12px rgba(66, 41, 159, 12%);
|
||||
--shadow-reversed: 0 -4px 12px rgba(66, 41, 159, 12%);
|
||||
|
||||
// legacy below
|
||||
--color-toast: rgba(247, 248, 248, 80%);
|
||||
--color-overlay: rgba(25, 28, 29, 40%);
|
||||
|
|
|
@ -39,7 +39,7 @@ const getSignInExperienceSettings = async <
|
|||
termsOfUse,
|
||||
primarySignInMethod: getPrimarySignInMethod(signInMethods),
|
||||
secondarySignInMethods: getSecondarySignInMethods(signInMethods),
|
||||
socialConnectors, // TODO: get values from api
|
||||
socialConnectors,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue