File: /var/www/vhosts/uyarreklam.com.tr/httpdocs/text-input.tar
style.scss 0000644 00000005373 15155674516 0006630 0 ustar 00 .wc-block-components-form .wc-block-components-text-input,
.wc-block-components-text-input {
position: relative;
margin-top: em($gap-large);
white-space: nowrap;
label {
@include reset-color();
@include reset-typography();
@include font-size(regular);
position: absolute;
transform: translateY(0.75em);
left: 0;
top: 0;
transform-origin: top left;
line-height: 1.375; // =22px when font-size is 16px.
color: $gray-700;
transition: transform 200ms ease;
margin: 0 0 0 #{$gap + 1px};
overflow: hidden;
text-overflow: ellipsis;
max-width: calc(100% - #{2 * $gap});
cursor: text;
.has-dark-controls & {
color: $input-placeholder-dark;
}
@media screen and (prefers-reduced-motion: reduce) {
transition: none;
}
}
input:-webkit-autofill + label {
transform: translateY(#{$gap-smallest}) scale(0.75);
}
&.is-active label {
transform: translateY(#{$gap-smallest}) scale(0.75);
}
input[type="tel"],
input[type="url"],
input[type="text"],
input[type="number"],
input[type="email"] {
@include font-size(regular);
background-color: #fff;
padding: em($gap-small) 0;
text-indent: $gap;
border-radius: 4px;
border: 1px solid $input-border-gray;
width: 100%;
line-height: 1.375; // =22px when font-size is 16px.
font-family: inherit;
margin: 0;
box-sizing: border-box;
height: 3em;
min-height: 0;
color: $input-text-active;
&:focus {
background-color: #fff;
color: $input-text-active;
outline: 0;
box-shadow: 0 0 0 1px $input-border-gray;
}
.has-dark-controls & {
background-color: $input-background-dark;
border-color: $input-border-dark;
color: $input-text-dark;
&:focus {
background-color: $input-background-dark;
color: $input-text-dark;
box-shadow: 0 0 0 1px $input-border-dark;
}
}
}
input[type="number"] {
-moz-appearance: textfield;
&::-webkit-outer-spin-button,
&::-webkit-inner-spin-button {
appearance: none;
margin: 0;
}
}
&.is-active input[type="tel"],
&.is-active input[type="url"],
&.is-active input[type="text"],
&.is-active input[type="number"],
&.is-active input[type="email"] {
padding: em($gap-large) 0 em($gap-smallest);
}
&.has-error input {
&,
&:hover,
&:focus,
&:active {
border-color: $alert-red;
}
&:focus {
box-shadow: 0 0 0 1px $alert-red;
}
.has-dark-controls &,
.has-dark-controls &:hover,
.has-dark-controls &:focus,
.has-dark-controls &:active {
border-color: color.adjust($alert-red, $lightness: 30%);
}
.has-dark-controls &:focus {
box-shadow: 0 0 0 1px color.adjust($alert-red, $lightness: 30%);
}
}
&.has-error label {
color: $alert-red;
.has-dark-controls & {
color: color.adjust($alert-red, $lightness: 30%);
}
}
&:only-child {
margin-top: 1.5em;
}
}
test/validated-text-input.tsx 0000644 00000021571 15155674516 0012364 0 ustar 00 /**
* External dependencies
*/
import { act, render, screen } from '@testing-library/react';
import { VALIDATION_STORE_KEY } from '@woocommerce/block-data';
import { dispatch, select } from '@wordpress/data';
import userEvent from '@testing-library/user-event';
import { useState } from '@wordpress/element';
import * as wpData from '@wordpress/data';
/**
* Internal dependencies
*/
import ValidatedTextInput from '../validated-text-input';
jest.mock( '@wordpress/data', () => ( {
__esModule: true,
...jest.requireActual( '@wordpress/data' ),
useDispatch: jest.fn().mockImplementation( ( args ) => {
return jest.requireActual( '@wordpress/data' ).useDispatch( args );
} ),
} ) );
describe( 'ValidatedTextInput', () => {
it( 'Removes related validation error on change', async () => {
render(
<ValidatedTextInput
instanceId={ '0' }
accept={ 'image/*' }
onChange={ () => void 0 }
value={ 'Test' }
id={ 'test-input' }
label={ 'Test Input' }
/>
);
await act( () =>
dispatch( VALIDATION_STORE_KEY ).setValidationErrors( {
'test-input': {
message: 'Error message',
hidden: false,
},
} )
);
await expect(
select( VALIDATION_STORE_KEY ).getValidationError( 'test-input' )
).not.toBe( undefined );
const textInputElement = await screen.getByLabelText( 'Test Input' );
await userEvent.type( textInputElement, 'New value' );
await expect(
select( VALIDATION_STORE_KEY ).getValidationError( 'test-input' )
).toBe( undefined );
} );
it( 'Hides related validation error on change when id is not specified', async () => {
render(
<ValidatedTextInput
instanceId={ '1' }
accept={ 'image/*' }
onChange={ () => void 0 }
value={ 'Test' }
label={ 'Test Input' }
/>
);
await act( () =>
dispatch( VALIDATION_STORE_KEY ).setValidationErrors( {
'textinput-1': {
message: 'Error message',
hidden: false,
},
} )
);
await expect(
select( VALIDATION_STORE_KEY ).getValidationError( 'textinput-1' )
).not.toBe( undefined );
const textInputElement = await screen.getByLabelText( 'Test Input' );
await userEvent.type( textInputElement, 'New value' );
await expect(
select( VALIDATION_STORE_KEY ).getValidationError( 'textinput-1' )
).toBe( undefined );
} );
it( 'Displays a passed error message', async () => {
render(
<ValidatedTextInput
instanceId={ '2' }
accept={ 'image/*' }
onChange={ () => void 0 }
value={ 'Test' }
label={ 'Test Input' }
errorMessage={ 'Custom error message' }
/>
);
await act( () =>
dispatch( VALIDATION_STORE_KEY ).setValidationErrors( {
'textinput-2': {
message: 'Error message in data store',
hidden: false,
},
} )
);
const customErrorMessageElement = await screen.getByText(
'Custom error message'
);
expect(
screen.queryByText( 'Error message in data store' )
).not.toBeInTheDocument();
await expect( customErrorMessageElement ).toBeInTheDocument();
} );
it( 'Displays an error message from the data store', async () => {
render(
<ValidatedTextInput
instanceId={ '3' }
accept={ 'image/*' }
onChange={ () => void 0 }
value={ 'Test' }
label={ 'Test Input' }
/>
);
await act( () =>
dispatch( VALIDATION_STORE_KEY ).setValidationErrors( {
'textinput-3': {
message: 'Error message 3',
hidden: false,
},
} )
);
const errorMessageElement = await screen.getByText( 'Error message 3' );
await expect( errorMessageElement ).toBeInTheDocument();
} );
it( 'Runs custom validation on the input', async () => {
const TestComponent = () => {
const [ inputValue, setInputValue ] = useState( 'Test' );
return (
<ValidatedTextInput
instanceId={ '4' }
id={ 'test-input' }
onChange={ ( value ) => setInputValue( value ) }
value={ inputValue }
label={ 'Test Input' }
customValidation={ ( inputObject ) => {
return inputObject.value === 'Valid Value';
} }
/>
);
};
render( <TestComponent /> );
const textInputElement = await screen.getByLabelText( 'Test Input' );
await userEvent.type( textInputElement, 'Invalid Value' );
await expect(
select( VALIDATION_STORE_KEY ).getValidationError( 'test-input' )
).not.toBe( undefined );
await userEvent.type( textInputElement, '{selectall}{del}Valid Value' );
await expect( textInputElement.value ).toBe( 'Valid Value' );
await expect(
select( VALIDATION_STORE_KEY ).getValidationError( 'test-input' )
).toBe( undefined );
} );
it( 'Shows a custom error message for an invalid required input', async () => {
const TestComponent = () => {
const [ inputValue, setInputValue ] = useState( '' );
return (
<ValidatedTextInput
instanceId={ '5' }
id={ 'test-input' }
onChange={ ( value ) => setInputValue( value ) }
value={ inputValue }
label={ 'Test Input' }
required={ true }
/>
);
};
render( <TestComponent /> );
const textInputElement = await screen.getByLabelText( 'Test Input' );
await userEvent.type( textInputElement, 'test' );
await userEvent.type( textInputElement, '{selectall}{del}' );
await textInputElement.blur();
await expect(
screen.queryByText( 'Please enter a valid test input' )
).not.toBeNull();
} );
describe( 'correctly validates on mount', () => {
it( 'validates when focusOnMount is true and validateOnMount is not set', async () => {
const setValidationErrors = jest.fn();
wpData.useDispatch.mockImplementation( ( storeName: string ) => {
if ( storeName === VALIDATION_STORE_KEY ) {
return {
...jest
.requireActual( '@wordpress/data' )
.useDispatch( storeName ),
setValidationErrors,
};
}
return jest
.requireActual( '@wordpress/data' )
.useDispatch( storeName );
} );
const TestComponent = () => {
const [ inputValue, setInputValue ] = useState( '' );
return (
<ValidatedTextInput
instanceId={ '6' }
id={ 'test-input' }
onChange={ ( value ) => setInputValue( value ) }
value={ inputValue }
label={ 'Test Input' }
required={ true }
focusOnMount={ true }
/>
);
};
await render( <TestComponent /> );
const textInputElement = await screen.getByLabelText(
'Test Input'
);
await expect( textInputElement ).toHaveFocus();
await expect( setValidationErrors ).toHaveBeenCalledWith( {
'test-input': {
message: 'Please enter a valid test input',
hidden: true,
},
} );
} );
it( 'validates when focusOnMount is false, regardless of validateOnMount value', async () => {
const setValidationErrors = jest.fn();
wpData.useDispatch.mockImplementation( ( storeName: string ) => {
if ( storeName === VALIDATION_STORE_KEY ) {
return {
...jest
.requireActual( '@wordpress/data' )
.useDispatch( storeName ),
setValidationErrors,
};
}
return jest
.requireActual( '@wordpress/data' )
.useDispatch( storeName );
} );
const TestComponent = ( { validateOnMount = false } ) => {
const [ inputValue, setInputValue ] = useState( '' );
return (
<ValidatedTextInput
instanceId={ '6' }
id={ 'test-input' }
onChange={ ( value ) => setInputValue( value ) }
value={ inputValue }
label={ 'Test Input' }
required={ true }
focusOnMount={ true }
validateOnMount={ validateOnMount }
/>
);
};
const { rerender } = await render( <TestComponent /> );
const textInputElement = await screen.getByLabelText(
'Test Input'
);
await expect( textInputElement ).toHaveFocus();
await expect( setValidationErrors ).not.toHaveBeenCalled();
await rerender( <TestComponent validateOnMount={ true } /> );
await expect( textInputElement ).toHaveFocus();
await expect( setValidationErrors ).not.toHaveBeenCalled();
} );
it( 'does not validate when validateOnMount is false and focusOnMount is true', async () => {
const setValidationErrors = jest.fn();
wpData.useDispatch.mockImplementation( ( storeName: string ) => {
if ( storeName === VALIDATION_STORE_KEY ) {
return {
...jest
.requireActual( '@wordpress/data' )
.useDispatch( storeName ),
setValidationErrors,
};
}
return jest
.requireActual( '@wordpress/data' )
.useDispatch( storeName );
} );
const TestComponent = () => {
const [ inputValue, setInputValue ] = useState( '' );
return (
<ValidatedTextInput
instanceId={ '6' }
id={ 'test-input' }
onChange={ ( value ) => setInputValue( value ) }
value={ inputValue }
label={ 'Test Input' }
required={ true }
focusOnMount={ true }
validateOnMount={ false }
/>
);
};
await render( <TestComponent /> );
const textInputElement = await screen.getByLabelText(
'Test Input'
);
await expect( textInputElement ).toHaveFocus();
await expect( setValidationErrors ).not.toHaveBeenCalled();
} );
} );
} );
text-input.tsx 0000644 00000004524 15155674516 0007451 0 ustar 00 /**
* External dependencies
*/
import classnames from 'classnames';
import { forwardRef, useState } from '@wordpress/element';
import { decodeEntities } from '@wordpress/html-entities';
import type { InputHTMLAttributes } from 'react';
/**
* Internal dependencies
*/
import Label from '../label';
import './style.scss';
interface TextInputProps
extends Omit<
InputHTMLAttributes< HTMLInputElement >,
'onChange' | 'onBlur'
> {
id: string;
ariaLabel?: string;
label?: string | undefined;
ariaDescribedBy?: string | undefined;
screenReaderLabel?: string;
help?: string;
feedback?: boolean | JSX.Element;
autoComplete?: string | undefined;
onChange: ( newValue: string ) => void;
onBlur?: ( newValue: string ) => void;
}
const TextInput = forwardRef< HTMLInputElement, TextInputProps >(
(
{
className,
id,
type = 'text',
ariaLabel,
ariaDescribedBy,
label,
screenReaderLabel,
disabled,
help,
autoCapitalize = 'off',
autoComplete = 'off',
value = '',
onChange,
required = false,
onBlur = () => {
/* Do nothing */
},
feedback,
...rest
},
ref
) => {
const [ isActive, setIsActive ] = useState( false );
return (
<div
className={ classnames(
'wc-block-components-text-input',
className,
{
'is-active': isActive || value,
}
) }
>
<input
type={ type }
id={ id }
value={ decodeEntities( value ) }
ref={ ref }
autoCapitalize={ autoCapitalize }
autoComplete={ autoComplete }
onChange={ ( event ) => {
onChange( event.target.value );
} }
onFocus={ () => setIsActive( true ) }
onBlur={ ( event ) => {
onBlur( event.target.value );
setIsActive( false );
} }
aria-label={ ariaLabel || label }
disabled={ disabled }
aria-describedby={
!! help && ! ariaDescribedBy
? id + '__help'
: ariaDescribedBy
}
required={ required }
{ ...rest }
/>
<Label
label={ label }
screenReaderLabel={ screenReaderLabel || label }
wrapperElement="label"
wrapperProps={ {
htmlFor: id,
} }
htmlFor={ id }
/>
{ !! help && (
<p
id={ id + '__help' }
className="wc-block-components-text-input__help"
>
{ help }
</p>
) }
{ feedback }
</div>
);
}
);
export default TextInput;
types.ts 0000644 00000002770 15155674516 0006305 0 ustar 00 /**
* External dependencies
*/
import type { InputHTMLAttributes } from 'react';
export interface ValidatedTextInputProps
extends Omit<
InputHTMLAttributes< HTMLInputElement >,
'onChange' | 'onBlur'
> {
// id to use for the input. If not provided, an id will be generated.
id?: string;
// Unique instance ID. id will be used instead if provided.
instanceId: string;
// Class name to add to the input.
className?: string | undefined;
// aria-describedby attribute to add to the input.
ariaDescribedBy?: string | undefined;
// id to use for the error message. If not provided, an id will be generated.
errorId?: string;
// if true, the input will be focused on mount.
focusOnMount?: boolean;
// Callback to run on change which is passed the updated value.
onChange: ( newValue: string ) => void;
// Optional label for the field.
label?: string | undefined;
// Field value.
value: string;
// If true, validation errors will be shown.
showError?: boolean;
// Error message to display alongside the field regardless of validation.
errorMessage?: string | undefined;
// Custom validation function that is run on change. Use setCustomValidity to set an error message.
customValidation?:
| ( ( inputObject: HTMLInputElement ) => boolean )
| undefined;
// Custom formatted to format values as they are typed.
customFormatter?: ( value: string ) => string;
// Whether validation should run when focused - only has an effect when focusOnMount is also true.
validateOnMount?: boolean | undefined;
}
validated-text-input.tsx 0000644 00000014051 15155674516 0011400 0 ustar 00 /**
* External dependencies
*/
import {
useEffect,
useState,
useCallback,
forwardRef,
useImperativeHandle,
useRef,
} from '@wordpress/element';
import classnames from 'classnames';
import { isObject } from '@woocommerce/types';
import { useDispatch, useSelect } from '@wordpress/data';
import { VALIDATION_STORE_KEY } from '@woocommerce/block-data';
import { usePrevious } from '@woocommerce/base-hooks';
import { useInstanceId } from '@wordpress/compose';
/**
* Internal dependencies
*/
import TextInput from './text-input';
import './style.scss';
import { ValidationInputError } from '../validation-input-error';
import { getValidityMessageForInput } from '../../utils';
import { ValidatedTextInputProps } from './types';
export type ValidatedTextInputHandle = {
revalidate: () => void;
};
/**
* A text based input which validates the input value.
*/
const ValidatedTextInput = forwardRef<
ValidatedTextInputHandle,
ValidatedTextInputProps
>(
(
{
className,
id,
ariaDescribedBy,
errorId,
focusOnMount = false,
onChange,
showError = true,
errorMessage: passedErrorMessage = '',
value = '',
customValidation = () => true,
customFormatter = ( newValue: string ) => newValue,
label,
validateOnMount = true,
instanceId: preferredInstanceId,
...rest
},
forwardedRef
): JSX.Element => {
// True on mount.
const [ isPristine, setIsPristine ] = useState( true );
// Track incoming value.
const previousValue = usePrevious( value );
// Ref for the input element.
const inputRef = useRef< HTMLInputElement >( null );
const instanceId = useInstanceId(
ValidatedTextInput,
'',
preferredInstanceId
);
const textInputId =
typeof id !== 'undefined' ? id : 'textinput-' + instanceId;
const errorIdString = errorId !== undefined ? errorId : textInputId;
const {
setValidationErrors,
hideValidationError,
clearValidationError,
} = useDispatch( VALIDATION_STORE_KEY );
const { validationError, validationErrorId } = useSelect(
( select ) => {
const store = select( VALIDATION_STORE_KEY );
return {
validationError: store.getValidationError( errorIdString ),
validationErrorId:
store.getValidationErrorId( errorIdString ),
};
}
);
const validateInput = useCallback(
( errorsHidden = true ) => {
const inputObject = inputRef.current || null;
if ( inputObject === null ) {
return;
}
// Trim white space before validation.
inputObject.value = inputObject.value.trim();
inputObject.setCustomValidity( '' );
if (
inputObject.checkValidity() &&
customValidation( inputObject )
) {
clearValidationError( errorIdString );
return;
}
setValidationErrors( {
[ errorIdString ]: {
message: label
? getValidityMessageForInput( label, inputObject )
: inputObject.validationMessage,
hidden: errorsHidden,
},
} );
},
[
clearValidationError,
customValidation,
errorIdString,
setValidationErrors,
label,
]
);
// Allows parent to trigger revalidation.
useImperativeHandle(
forwardedRef,
function () {
return {
revalidate() {
validateInput( ! value );
},
};
},
[ validateInput, value ]
);
/**
* Handle browser autofill / changes via data store.
*
* Trigger validation on incoming state change if the current element is not in focus. This is because autofilled
* elements do not trigger the blur() event, and so values can be validated in the background if the state changes
* elsewhere.
*
* Errors are immediately visible.
*/
useEffect( () => {
if (
value !== previousValue &&
( value || previousValue ) &&
inputRef &&
inputRef.current !== null &&
inputRef.current?.ownerDocument?.activeElement !==
inputRef.current
) {
const formattedValue = customFormatter(
inputRef.current.value
);
if ( formattedValue !== value ) {
onChange( formattedValue );
}
}
}, [ validateInput, customFormatter, value, previousValue, onChange ] );
/**
* Validation on mount.
*
* If the input is in pristine state on mount, focus the element (if focusOnMount is enabled), and validate in the
* background.
*
* Errors are hidden until blur.
*/
useEffect( () => {
if ( ! isPristine ) {
return;
}
if ( focusOnMount ) {
inputRef.current?.focus();
}
// if validateOnMount is false, only validate input if focusOnMount is also false
if ( validateOnMount || ! focusOnMount ) {
validateInput( true );
}
setIsPristine( false );
}, [
validateOnMount,
focusOnMount,
isPristine,
setIsPristine,
validateInput,
] );
// Remove validation errors when unmounted.
useEffect( () => {
return () => {
clearValidationError( errorIdString );
};
}, [ clearValidationError, errorIdString ] );
if ( passedErrorMessage !== '' && isObject( validationError ) ) {
validationError.message = passedErrorMessage;
}
const hasError = validationError?.message && ! validationError?.hidden;
const describedBy =
showError && hasError && validationErrorId
? validationErrorId
: ariaDescribedBy;
return (
<TextInput
className={ classnames( className, {
'has-error': hasError,
} ) }
aria-invalid={ hasError === true }
id={ textInputId }
feedback={
showError && (
<ValidationInputError
errorMessage={ passedErrorMessage }
propertyName={ errorIdString }
/>
)
}
ref={ inputRef }
onChange={ ( newValue ) => {
// Hide errors while typing.
hideValidationError( errorIdString );
// Validate the input value.
validateInput( true );
// Push the changes up to the parent component.
const formattedValue = customFormatter( newValue );
if ( formattedValue !== value ) {
onChange( formattedValue );
}
} }
onBlur={ () => validateInput( false ) }
ariaDescribedBy={ describedBy }
value={ value }
title=""
label={ label }
{ ...rest }
/>
);
}
);
export default ValidatedTextInput;