0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

fix(ui): update SmartInput animation (#3216)

This commit is contained in:
simeng-li 2023-02-24 17:53:34 +08:00 committed by GitHub
parent d240d26031
commit ab65d895b5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 38 additions and 15 deletions

View file

@ -288,3 +288,17 @@ export const mockSignInMethodSettingsTestCases: Array<SignIn['methods']> = [
}, },
], ],
]; ];
export const getBoundingClientRectMock = (mock: Partial<DOMRect>) =>
jest.fn(() => ({
width: 0,
height: 0,
x: 0,
y: 0,
top: 0,
bottom: 0,
left: 0,
right: 0,
...mock,
toJSON: jest.fn(),
}));

View file

@ -1,7 +1,7 @@
import { onResize, useIsomorphicLayoutEffect } from '@react-spring/shared'; import { onResize, useIsomorphicLayoutEffect } from '@react-spring/shared';
import { useSpring, animated, config } from '@react-spring/web'; import { useSpring, animated, config } from '@react-spring/web';
import type { Nullable } from '@silverhand/essentials'; import type { Nullable } from '@silverhand/essentials';
import { cloneElement, useCallback, useRef } from 'react'; import { cloneElement, useCallback, useRef, useState } from 'react';
import * as styles from './index.module.scss'; import * as styles from './index.module.scss';
@ -11,6 +11,7 @@ type Props = {
}; };
const AnimatedPrefix = ({ children, isVisible }: Props) => { const AnimatedPrefix = ({ children, isVisible }: Props) => {
const [domReady, setDomReady] = useState(false);
const elementRef = useRef<Nullable<HTMLElement>>(null); const elementRef = useRef<Nullable<HTMLElement>>(null);
// Get target width for the children // Get target width for the children
@ -21,11 +22,14 @@ const AnimatedPrefix = ({ children, isVisible }: Props) => {
const [animation, api] = useSpring( const [animation, api] = useSpring(
() => ({ width: getTargetWidth(), config: { ...config.default, clamp: true } }), () => ({ width: getTargetWidth(), config: { ...config.default, clamp: true } }),
[getTargetWidth] [getTargetWidth, domReady]
); );
useIsomorphicLayoutEffect(() => { useIsomorphicLayoutEffect(() => {
const { current } = elementRef; const { current } = elementRef;
setDomReady(true);
const cleanup = const cleanup =
current && current &&
onResize( onResize(

View file

@ -3,6 +3,7 @@ import { Globals } from '@react-spring/web';
import { assert } from '@silverhand/essentials'; import { assert } from '@silverhand/essentials';
import { fireEvent, render } from '@testing-library/react'; import { fireEvent, render } from '@testing-library/react';
import { getBoundingClientRectMock } from '@/__mocks__/logto';
import { getDefaultCountryCallingCode } from '@/utils/country-code'; import { getDefaultCountryCallingCode } from '@/utils/country-code';
import type { IdentifierInputType } from '.'; import type { IdentifierInputType } from '.';
@ -15,6 +16,7 @@ jest.mock('i18next', () => ({
describe('SmartInputField Component', () => { describe('SmartInputField Component', () => {
const onChange = jest.fn(); const onChange = jest.fn();
const defaultCountryCallingCode = getDefaultCountryCallingCode(); const defaultCountryCallingCode = getDefaultCountryCallingCode();
const renderInputField = (props: { const renderInputField = (props: {
@ -27,6 +29,11 @@ describe('SmartInputField Component', () => {
Globals.assign({ Globals.assign({
skipAnimation: true, skipAnimation: true,
}); });
// eslint-disable-next-line @silverhand/fp/no-mutation
window.HTMLDivElement.prototype.getBoundingClientRect = getBoundingClientRectMock({
width: 100,
});
}); });
afterEach(() => { afterEach(() => {
@ -65,10 +72,7 @@ describe('SmartInputField Component', () => {
const countryCode = queryAllByText(`+${defaultCountryCallingCode}`); const countryCode = queryAllByText(`+${defaultCountryCallingCode}`);
expect(countryCode).toHaveLength(2); expect(countryCode).toHaveLength(2);
// Country code select should have a >0 width. expect(queryByTestId('prefix')?.style.width).toBe('100px');
// The React Spring acquires the child element's width ahead of elementRef is properly set.
// So the value returns null. Assert style is null to represent the width is >0.
expect(queryByTestId('prefix')?.getAttribute('style')).toBe(null);
const selector = container.querySelector('select'); const selector = container.querySelector('select');
assert(selector, new Error('selector should not be null')); assert(selector, new Error('selector should not be null'));

View file

@ -5,7 +5,7 @@ import { MemoryRouter, useLocation } from 'react-router-dom';
import renderWithPageContext from '@/__mocks__/RenderWithPageContext'; import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
import SettingsProvider from '@/__mocks__/RenderWithPageContext/SettingsProvider'; import SettingsProvider from '@/__mocks__/RenderWithPageContext/SettingsProvider';
import { mockSignInExperienceSettings } from '@/__mocks__/logto'; import { mockSignInExperienceSettings, getBoundingClientRectMock } from '@/__mocks__/logto';
import type { SignInExperienceResponse } from '@/types'; import type { SignInExperienceResponse } from '@/types';
import ForgotPassword from '.'; import ForgotPassword from '.';
@ -43,6 +43,11 @@ describe('ForgotPassword', () => {
Globals.assign({ Globals.assign({
skipAnimation: true, skipAnimation: true,
}); });
// eslint-disable-next-line @silverhand/fp/no-mutation
window.HTMLDivElement.prototype.getBoundingClientRect = getBoundingClientRectMock({
width: 100,
});
}); });
afterEach(() => { afterEach(() => {
@ -79,6 +84,7 @@ describe('ForgotPassword', () => {
const { queryByText, queryAllByText, container, queryByTestId } = renderPage(settings); const { queryByText, queryAllByText, container, queryByTestId } = renderPage(settings);
const inputField = container.querySelector('input[name="identifier"]'); const inputField = container.querySelector('input[name="identifier"]');
const countryCodeSelectorPrefix = queryByTestId('prefix'); const countryCodeSelectorPrefix = queryByTestId('prefix');
assert(inputField, new Error('input field not found')); assert(inputField, new Error('input field not found'));
expect(queryByText('description.reset_password')).not.toBeNull(); expect(queryByText('description.reset_password')).not.toBeNull();
@ -88,12 +94,7 @@ describe('ForgotPassword', () => {
if (state.identifier === SignInIdentifier.Phone && settings.phone) { if (state.identifier === SignInIdentifier.Phone && settings.phone) {
expect(inputField.getAttribute('value')).toBe(phone); expect(inputField.getAttribute('value')).toBe(phone);
expect(countryCodeSelectorPrefix?.style.width).toBe('100px');
// Country code select should have a >0 width.
// The React Spring acquires the child element's width ahead of elementRef is properly set.
// So the value returns null. Assert style is null to represent the width is >0.
expect(countryCodeSelectorPrefix?.getAttribute('style')).toBeNull();
expect(queryAllByText(`+${countryCode}`)).toHaveLength(2); expect(queryAllByText(`+${countryCode}`)).toHaveLength(2);
} else if (state.identifier === SignInIdentifier.Phone) { } else if (state.identifier === SignInIdentifier.Phone) {
// Phone Number not enabled // Phone Number not enabled
@ -107,7 +108,7 @@ describe('ForgotPassword', () => {
} else if (state.identifier === SignInIdentifier.Email) { } else if (state.identifier === SignInIdentifier.Email) {
// Only PhoneNumber is enabled // Only PhoneNumber is enabled
expect(inputField.getAttribute('value')).toBe(''); expect(inputField.getAttribute('value')).toBe('');
expect(countryCodeSelectorPrefix?.getAttribute('style')).toBeNull(); expect(countryCodeSelectorPrefix?.style.width).toBe('100px');
} }
if (state.identifier === SignInIdentifier.Username && settings.email) { if (state.identifier === SignInIdentifier.Username && settings.email) {
@ -116,7 +117,7 @@ describe('ForgotPassword', () => {
} else if (state.identifier === SignInIdentifier.Username) { } else if (state.identifier === SignInIdentifier.Username) {
// Only PhoneNumber is enabled // Only PhoneNumber is enabled
expect(inputField.getAttribute('value')).toBe(''); expect(inputField.getAttribute('value')).toBe('');
expect(countryCodeSelectorPrefix?.getAttribute('style')).toBeNull(); expect(countryCodeSelectorPrefix?.style.width).toBe('100px');
} }
}); });
}); });