diff --git a/.changeset-staged/good-actors-reflect.md b/.changeset-staged/good-actors-reflect.md new file mode 100644 index 000000000..a311c14d3 --- /dev/null +++ b/.changeset-staged/good-actors-reflect.md @@ -0,0 +1,7 @@ +--- +"@logto/ui": minor +--- + +## Add iframe modal for mobile platform + +Implement a full screen iframe modal on the mobile platform. As for most of the webview containers, opening a new tab is not allowed. So we need to implement a full screen iframe modal to show the external link page on the mobile platform. diff --git a/.changeset-staged/mighty-dodos-admire.md b/.changeset-staged/mighty-dodos-admire.md new file mode 100644 index 000000000..49e974700 --- /dev/null +++ b/.changeset-staged/mighty-dodos-admire.md @@ -0,0 +1,6 @@ +--- +"@logto/phrases-ui": minor +"@logto/ui": minor +--- + +Implement a country code selector dropdown component with search box. Users may able to quick search for a country code by typing in the search box. diff --git a/packages/phrases-ui/src/locales/de.ts b/packages/phrases-ui/src/locales/de.ts index 6736b3ab0..5efb3558b 100644 --- a/packages/phrases-ui/src/locales/de.ts +++ b/packages/phrases-ui/src/locales/de.ts @@ -7,6 +7,7 @@ const translation = { email: 'Email', phone_number: 'Telefonnummer', confirm_password: 'Passwort bestätigen', + search_region_code: 'Suche region code', }, secondary: { social_bind_with: diff --git a/packages/phrases-ui/src/locales/en.ts b/packages/phrases-ui/src/locales/en.ts index ddcc57439..8ddf72ee0 100644 --- a/packages/phrases-ui/src/locales/en.ts +++ b/packages/phrases-ui/src/locales/en.ts @@ -5,6 +5,7 @@ const translation = { email: 'Email', phone_number: 'Phone number', confirm_password: 'Confirm password', + search_region_code: 'Search region code', }, secondary: { social_bind_with: diff --git a/packages/phrases-ui/src/locales/fr.ts b/packages/phrases-ui/src/locales/fr.ts index 3e313d60f..50b8d14b6 100644 --- a/packages/phrases-ui/src/locales/fr.ts +++ b/packages/phrases-ui/src/locales/fr.ts @@ -7,6 +7,7 @@ const translation = { email: 'Email', phone_number: 'Numéro de téléphone', confirm_password: 'Confirmer le mot de passe', + search_region_code: 'Rechercher le code de région', }, secondary: { social_bind_with: diff --git a/packages/phrases-ui/src/locales/ko.ts b/packages/phrases-ui/src/locales/ko.ts index 258c0dd12..a2600f649 100644 --- a/packages/phrases-ui/src/locales/ko.ts +++ b/packages/phrases-ui/src/locales/ko.ts @@ -7,6 +7,7 @@ const translation = { email: '이메일', phone_number: '휴대전화번호', confirm_password: '비밀번호 확인', + search_region_code: '지역 코드 검색', }, secondary: { social_bind_with: diff --git a/packages/phrases-ui/src/locales/pt-br.ts b/packages/phrases-ui/src/locales/pt-br.ts index fee3ecbd8..b87e360a0 100644 --- a/packages/phrases-ui/src/locales/pt-br.ts +++ b/packages/phrases-ui/src/locales/pt-br.ts @@ -7,6 +7,7 @@ const translation = { email: 'E-mail', phone_number: 'Número de telefone', confirm_password: 'Confirme a senha', + search_region_code: 'Pesquisar código de região', }, secondary: { social_bind_with: diff --git a/packages/phrases-ui/src/locales/pt-pt.ts b/packages/phrases-ui/src/locales/pt-pt.ts index 72e895278..de1ac635c 100644 --- a/packages/phrases-ui/src/locales/pt-pt.ts +++ b/packages/phrases-ui/src/locales/pt-pt.ts @@ -7,6 +7,7 @@ const translation = { email: 'Email', phone_number: 'Telefone', confirm_password: 'Confirmar palavra-passe', + search_region_code: 'Procurar código de região', }, secondary: { social_bind_with: diff --git a/packages/phrases-ui/src/locales/ru.ts b/packages/phrases-ui/src/locales/ru.ts index 22dc16c27..d73aaa84e 100644 --- a/packages/phrases-ui/src/locales/ru.ts +++ b/packages/phrases-ui/src/locales/ru.ts @@ -7,6 +7,7 @@ const translation = { email: 'Электронная почта', phone_number: 'Номер телефона', confirm_password: 'Подтверждение пароля', + search_region_code: 'Поиск кода региона', }, secondary: { social_bind_with: diff --git a/packages/phrases-ui/src/locales/tr-tr.ts b/packages/phrases-ui/src/locales/tr-tr.ts index b478c5c18..81910aa2b 100644 --- a/packages/phrases-ui/src/locales/tr-tr.ts +++ b/packages/phrases-ui/src/locales/tr-tr.ts @@ -7,6 +7,7 @@ const translation = { email: 'E-posta Adresi', phone_number: 'Telefon Numarası', confirm_password: 'Şifreyi Doğrula', + search_region_code: 'Bölge kodunu ara', }, secondary: { social_bind_with: diff --git a/packages/phrases-ui/src/locales/zh-cn.ts b/packages/phrases-ui/src/locales/zh-cn.ts index ec58e4dbb..beff8bd55 100644 --- a/packages/phrases-ui/src/locales/zh-cn.ts +++ b/packages/phrases-ui/src/locales/zh-cn.ts @@ -7,6 +7,7 @@ const translation = { email: '邮箱', phone_number: '手机号', confirm_password: '确认密码', + search_region_code: '搜索区域码', }, secondary: { social_bind_with: diff --git a/packages/ui/package.json b/packages/ui/package.json index 13c44939f..cbbc5a787 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -74,6 +74,7 @@ "react-router-dom": "^6.2.2", "react-string-replace": "^1.0.0", "react-timer-hook": "^3.0.5", + "react-top-loading-bar": "^2.3.1", "stylelint": "^15.0.0", "superstruct": "^0.16.0", "ts-jest": "^29.0.5", diff --git a/packages/ui/src/Providers/AppBoundary/index.tsx b/packages/ui/src/Providers/AppBoundary/index.tsx index 4c3def0d3..609f45283 100644 --- a/packages/ui/src/Providers/AppBoundary/index.tsx +++ b/packages/ui/src/Providers/AppBoundary/index.tsx @@ -7,6 +7,7 @@ import { PageContext } from '@/hooks/use-page-context'; import useTheme from '@/hooks/use-theme'; import ConfirmModalProvider from '../ConfirmModalProvider'; +import IframeModalProvider from '../IframeModalProvider'; import ToastProvider from '../ToastProvider'; import * as styles from './index.module.scss'; @@ -18,6 +19,7 @@ const AppBoundary = ({ children }: Props) => { // Set Primary Color useColorTheme(); const theme = useTheme(); + const { platform } = useContext(PageContext); // Set Theme Mode @@ -33,9 +35,11 @@ const AppBoundary = ({ children }: Props) => { }, [platform]); return ( - - {children} - + + + {children} + + ); }; diff --git a/packages/ui/src/Providers/IframeModalProvider/IframeModal/index.module.scss b/packages/ui/src/Providers/IframeModalProvider/IframeModal/index.module.scss new file mode 100644 index 000000000..7577f8ec7 --- /dev/null +++ b/packages/ui/src/Providers/IframeModalProvider/IframeModal/index.module.scss @@ -0,0 +1,71 @@ +@use '@/scss/underscore' as _; + + +.overlay { + z-index: 101; +} + +.modal { + z-index: 101; + position: absolute; + inset: 0; + overflow: auto; +} + +.container { + background: var(--color-bg-body); + height: 100%; + @include _.flex-column; + align-items: stretch; + overflow: hidden; +} + +.modal, +.container { + &:focus-visible { + outline: none; + } +} + +.header { + padding: _.unit(2) _.unit(5); +} + +.content { + flex: 1; + width: 100%; +} + +iframe { + width: 100%; + height: 100%; + border: none; + background: var(--color-bg-body); + opacity: 0%; + transition: opacity 0.2s ease-in-out; + + &.loaded { + opacity: 100%; + } +} + +.loader { + background: var(--color-brand-default); +} + +/* stylelint-disable selector-class-pattern */ +:global { + .ReactModal__Content[id='iframe-modal'] { + transform: translateY(100%); + transition: transform 0.3s ease-in-out; + } + + .ReactModal__Content--after-open[id='iframe-modal'] { + transform: translateY(0); + } + + .ReactModal__Content--before-close[id='iframe-modal'] { + transform: translateY(100%); + } +} +/* stylelint-enable selector-class-pattern */ diff --git a/packages/ui/src/Providers/IframeModalProvider/IframeModal/index.tsx b/packages/ui/src/Providers/IframeModalProvider/IframeModal/index.tsx new file mode 100644 index 000000000..f7ce27988 --- /dev/null +++ b/packages/ui/src/Providers/IframeModalProvider/IframeModal/index.tsx @@ -0,0 +1,72 @@ +import { conditional } from '@silverhand/essentials'; +import classNames from 'classnames'; +import { useRef, useState } from 'react'; +import ReactModal from 'react-modal'; +import type { LoadingBarRef } from 'react-top-loading-bar'; +import LoadingBar from 'react-top-loading-bar'; + +import NavBar from '@/components/NavBar'; + +import * as styles from './index.module.scss'; + +type ModalProps = { + className?: string; + title?: string; + href?: string; + onClose: () => void; +}; + +const IframeModal = ({ className, title = '', href = '', onClose }: ModalProps) => { + const [isLoaded, setIsLoaded] = useState(false); + const loadingBarRef = useRef(null); + + const brandingColor = document.body.style.getPropertyValue('--color-brand-default') || '#5d34f2'; + + return ( + { + loadingBarRef.current?.continuousStart(); + }} + onRequestClose={onClose} + > +
+
+ +
+ +
+