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:
parent
d240d26031
commit
ab65d895b5
4 changed files with 38 additions and 15 deletions
|
@ -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(),
|
||||||
|
}));
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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'));
|
||||||
|
|
|
@ -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');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue