diff --git a/packages/ui/src/components/Dropdown/DropdownItem.module.scss b/packages/ui/src/components/Dropdown/DropdownItem.module.scss new file mode 100644 index 000000000..1f8daf934 --- /dev/null +++ b/packages/ui/src/components/Dropdown/DropdownItem.module.scss @@ -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); + } +} diff --git a/packages/ui/src/components/Dropdown/DropdownItem.tsx b/packages/ui/src/components/Dropdown/DropdownItem.tsx new file mode 100644 index 000000000..62d3e573f --- /dev/null +++ b/packages/ui/src/components/Dropdown/DropdownItem.tsx @@ -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) => ( +
  • + {children} +
  • +); + +export default DropdownItem; diff --git a/packages/ui/src/components/Dropdown/index.module.scss b/packages/ui/src/components/Dropdown/index.module.scss new file mode 100644 index 000000000..07153c254 --- /dev/null +++ b/packages/ui/src/components/Dropdown/index.module.scss @@ -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); +} diff --git a/packages/ui/src/components/Dropdown/index.tsx b/packages/ui/src/components/Dropdown/index.tsx new file mode 100644 index 000000000..caeba9f49 --- /dev/null +++ b/packages/ui/src/components/Dropdown/index.tsx @@ -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 ( + + + + ); +}; + +export default Dropdown; diff --git a/packages/ui/src/components/Icons/MoreSocialIcon.tsx b/packages/ui/src/components/Icons/MoreSocialIcon.tsx index 547ce421a..d5d851800 100644 --- a/packages/ui/src/components/Icons/MoreSocialIcon.tsx +++ b/packages/ui/src/components/Icons/MoreSocialIcon.tsx @@ -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) => ( - +const MoreSocialIcon = (props: SVGProps, reference?: Ref) => ( + ); -export default MoreSocialIcon; +export default forwardRef(MoreSocialIcon); diff --git a/packages/ui/src/containers/SocialSignIn/PrimarySocialSignIn.module.scss b/packages/ui/src/containers/SocialSignIn/PrimarySocialSignIn.module.scss new file mode 100644 index 000000000..25e80755e --- /dev/null +++ b/packages/ui/src/containers/SocialSignIn/PrimarySocialSignIn.module.scss @@ -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); + } +} diff --git a/packages/ui/src/containers/SocialSignIn/PrimarySocialSignIn.tsx b/packages/ui/src/containers/SocialSignIn/PrimarySocialSignIn.tsx index c1e1ca5c0..60408776f 100644 --- a/packages/ui/src/containers/SocialSignIn/PrimarySocialSignIn.tsx +++ b/packages/ui/src/containers/SocialSignIn/PrimarySocialSignIn.tsx @@ -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; diff --git a/packages/ui/src/containers/SocialSignIn/index.module.scss b/packages/ui/src/containers/SocialSignIn/SecondarySocialSignIn.module.scss similarity index 66% rename from packages/ui/src/containers/SocialSignIn/index.module.scss rename to packages/ui/src/containers/SocialSignIn/SecondarySocialSignIn.module.scss index ddc65a52d..fa51206dc 100644 --- a/packages/ui/src/containers/SocialSignIn/index.module.scss +++ b/packages/ui/src/containers/SocialSignIn/SecondarySocialSignIn.module.scss @@ -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; diff --git a/packages/ui/src/containers/SocialSignIn/SecondarySocialSignIn.tsx b/packages/ui/src/containers/SocialSignIn/SecondarySocialSignIn.tsx index 6f7b3bf8f..2006a7c03 100644 --- a/packages/ui/src/containers/SocialSignIn/SecondarySocialSignIn.tsx +++ b/packages/ui/src/containers/SocialSignIn/SecondarySocialSignIn.tsx @@ -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(null); const displayConnectors = useMemo(() => { if (isOverSize) { @@ -42,6 +45,7 @@ const SecondarySocialSignIn = ({ className }: Props) => { ))} {isOverSize && ( { setShowModal(true); @@ -49,7 +53,7 @@ const SecondarySocialSignIn = ({ className }: Props) => { /> )} - {isOverSize && ( + {isOverSize && isMobile && ( { @@ -57,6 +61,16 @@ const SecondarySocialSignIn = ({ className }: Props) => { }} /> )} + {isOverSize && !isMobile && ( + { + setShowModal(false); + }} + /> + )} ); }; diff --git a/packages/ui/src/containers/SocialSignIn/SocialSignInDropdown.module.scss b/packages/ui/src/containers/SocialSignIn/SocialSignInDropdown.module.scss new file mode 100644 index 000000000..d6ebee7e2 --- /dev/null +++ b/packages/ui/src/containers/SocialSignIn/SocialSignInDropdown.module.scss @@ -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 */ diff --git a/packages/ui/src/containers/SocialSignIn/SocialSignInDropdown.tsx b/packages/ui/src/containers/SocialSignIn/SocialSignInDropdown.tsx new file mode 100644 index 000000000..d467ec6ec --- /dev/null +++ b/packages/ui/src/containers/SocialSignIn/SocialSignInDropdown.tsx @@ -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; + 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 ( + { + void invokeSocialSignIn(id, onClose); + }} + > + {id} + {localName} + + ); + }), + [connectors, language, invokeSocialSignIn, onClose] + ); + + const adjustPosition = useCallback(() => { + if (anchorRef?.current) { + const { left, top } = anchorRef.current.getBoundingClientRect(); + + setContentStyle({ + left, + top: top - 8, + }); + } + }, [anchorRef]); + + return ( + { + setContentStyle(undefined); + }} + > + {items} + + ); +}; + +export default SocialSignInDropdown; diff --git a/packages/ui/src/containers/SocialSignIn/index.ts b/packages/ui/src/containers/SocialSignIn/index.ts index ae3d182ac..4e325477b 100644 --- a/packages/ui/src/containers/SocialSignIn/index.ts +++ b/packages/ui/src/containers/SocialSignIn/index.ts @@ -1,3 +1,2 @@ export { default as SecondarySocialSignIn } from './SecondarySocialSignIn'; export { default as PrimarySocialSignIn } from './PrimarySocialSignIn'; -export { default as SocialSignInPopUp } from './SocialSignInPopUp'; diff --git a/packages/ui/src/scss/_desktop.scss b/packages/ui/src/scss/_desktop.scss index a0476fa50..037328ae1 100644 --- a/packages/ui/src/scss/_desktop.scss +++ b/packages/ui/src/scss/_desktop.scss @@ -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%); diff --git a/packages/ui/src/utils/sign-in-experience.ts b/packages/ui/src/utils/sign-in-experience.ts index c76c43ce0..1d3a95b99 100644 --- a/packages/ui/src/utils/sign-in-experience.ts +++ b/packages/ui/src/utils/sign-in-experience.ts @@ -39,7 +39,7 @@ const getSignInExperienceSettings = async < termsOfUse, primarySignInMethod: getPrimarySignInMethod(signInMethods), secondarySignInMethods: getSecondarySignInMethods(signInMethods), - socialConnectors, // TODO: get values from api + socialConnectors, }; };