diff --git a/.changeset/heavy-students-study.md b/.changeset/heavy-students-study.md new file mode 100644 index 00000000000..1cc5dcf0b23 --- /dev/null +++ b/.changeset/heavy-students-study.md @@ -0,0 +1,5 @@ +--- +"@razorpay/blade": minor +--- + +feat(blade): add PhoneNumber input diff --git a/.github/actions/install-dependencies/action.yml b/.github/actions/install-dependencies/action.yml index dfec3eec9ce..89ed147ed7e 100644 --- a/.github/actions/install-dependencies/action.yml +++ b/.github/actions/install-dependencies/action.yml @@ -1,5 +1,6 @@ name: install npm packages description: Installs npm packages and manages it's cache + runs: using: composite steps: diff --git a/.github/workflows/blade-bundle-size.yml b/.github/workflows/blade-bundle-size.yml index d4d1e98af4e..0ac87bfdc8f 100644 --- a/.github/workflows/blade-bundle-size.yml +++ b/.github/workflows/blade-bundle-size.yml @@ -22,7 +22,11 @@ jobs: uses: actions/setup-node@v4 with: node-version: 18.12.1 + # https://stackoverflow.com/a/73824182 + registry-url: "https://npm.pkg.github.com" - name: Setup Cache & Install Dependencies + env: + NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} uses: ./.github/actions/install-dependencies - name: Update Bundle Size Data run: yarn generate-bundle-size-info @@ -47,7 +51,11 @@ jobs: uses: actions/setup-node@v4 with: node-version: 18.12.1 + # https://stackoverflow.com/a/73824182 + registry-url: "https://npm.pkg.github.com" - name: Setup Cache & Install Dependencies + env: + NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} uses: ./.github/actions/install-dependencies - name: Build Blade React Production run: yarn run-s build:clean build:generate-types build:react-prod diff --git a/.github/workflows/blade-chromatic.yml b/.github/workflows/blade-chromatic.yml index 977f7451d24..03d96be4bee 100644 --- a/.github/workflows/blade-chromatic.yml +++ b/.github/workflows/blade-chromatic.yml @@ -18,7 +18,11 @@ jobs: uses: actions/setup-node@v3 with: node-version: 18.12.1 + # https://stackoverflow.com/a/73824182 + registry-url: "https://npm.pkg.github.com" - name: Setup Cache & Install Dependencies + env: + NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} uses: ./.github/actions/install-dependencies - name: Publish to Chromatic uses: chromaui/action@v1 diff --git a/.github/workflows/blade-interaction-tests.yml b/.github/workflows/blade-interaction-tests.yml index e1553cca858..8a4200138d7 100644 --- a/.github/workflows/blade-interaction-tests.yml +++ b/.github/workflows/blade-interaction-tests.yml @@ -28,7 +28,11 @@ jobs: uses: actions/setup-node@v3 with: node-version: 18.12.1 + # https://stackoverflow.com/a/73824182 + registry-url: "https://npm.pkg.github.com" - name: Setup Cache & Install Dependencies + env: + NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} uses: ./.github/actions/install-dependencies - name: Run Interaction Tests run: | diff --git a/.github/workflows/blade-tokens-upload.yml b/.github/workflows/blade-tokens-upload.yml index a9d3866e596..8af157a5ab8 100644 --- a/.github/workflows/blade-tokens-upload.yml +++ b/.github/workflows/blade-tokens-upload.yml @@ -24,7 +24,11 @@ jobs: uses: actions/setup-node@v3 with: node-version: 18.12.1 + # https://stackoverflow.com/a/73824182 + registry-url: "https://npm.pkg.github.com" - name: Setup Cache & Install Dependencies + env: + NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} uses: ./.github/actions/install-dependencies - name: Upload Tokens working-directory: packages/blade diff --git a/.github/workflows/blade-validate.yml b/.github/workflows/blade-validate.yml index 021d0793cf5..7503e53f001 100644 --- a/.github/workflows/blade-validate.yml +++ b/.github/workflows/blade-validate.yml @@ -16,7 +16,11 @@ jobs: uses: actions/setup-node@v3 with: node-version: 18.12.1 + # https://stackoverflow.com/a/73824182 + registry-url: "https://npm.pkg.github.com" - name: Setup Cache & Install Dependencies + env: + NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} uses: ./.github/actions/install-dependencies - name: Build Blade run: yarn build @@ -41,7 +45,11 @@ jobs: uses: actions/setup-node@v3 with: node-version: 18.12.1 + # https://stackoverflow.com/a/73824182 + registry-url: "https://npm.pkg.github.com" - name: Setup Cache & Install Dependencies + env: + NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} uses: ./.github/actions/install-dependencies - name: Run Unit Tests run: yarn test diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f3f7b8cfdf4..399776a3b5e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,7 +21,11 @@ jobs: uses: actions/setup-node@v3 with: node-version: 18.12.1 + # https://stackoverflow.com/a/73824182 + registry-url: "https://npm.pkg.github.com" - name: Setup Cache & Install Dependencies + env: + NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} uses: ./.github/actions/install-dependencies - name: Create Release Pull Request & Publish packages id: changesets diff --git a/packages/blade/package.json b/packages/blade/package.json index 03f94a5a092..4e8c0d16e88 100644 --- a/packages/blade/package.json +++ b/packages/blade/package.json @@ -274,8 +274,8 @@ "typescript-transform-paths": "3.4.6", "@types/body-scroll-lock": "3.1.0", "ramda": "0.29.1", - "@razorpay/i18nify-js": "1.4.4", - "@razorpay/i18nify-react": "4.0.0" + "@razorpay/i18nify-js": "1.8.0", + "@razorpay/i18nify-react": "4.0.6" }, "peerDependencies": { "react": ">=18", @@ -291,7 +291,7 @@ "react-hot-toast": "2.4.1", "@gorhom/bottom-sheet": "^4.4.6", "@gorhom/portal": "^1.0.14", - "@razorpay/i18nify-js": "^1.4.4" + "@razorpay/i18nify-js": "^1.8.0" }, "peerDependenciesMeta": { "react-native": { diff --git a/packages/blade/src/components/Box/BaseBox/BaseBox.web.tsx b/packages/blade/src/components/Box/BaseBox/BaseBox.web.tsx index ec43803672b..2283fd7ca05 100644 --- a/packages/blade/src/components/Box/BaseBox/BaseBox.web.tsx +++ b/packages/blade/src/components/Box/BaseBox/BaseBox.web.tsx @@ -3,8 +3,9 @@ import type { BaseBoxProps } from './types'; import { useMemoizedStyles } from './useMemoizedStyles'; import { omitPropsFromHTML } from '~utils/omitPropsFromHTML'; import { metaAttribute, MetaConstants } from '~utils/metaAttribute'; +import { assignWithoutSideEffects } from '~utils/assignWithoutSideEffects'; -const BaseBox = styled.div +const _BaseBox = styled.div .attrs((props) => { return { ...metaAttribute({ @@ -21,4 +22,6 @@ const BaseBox = styled.div return cssObject; }); +const BaseBox = assignWithoutSideEffects(_BaseBox, { componentId: 'BaseBox' }); + export { BaseBox }; diff --git a/packages/blade/src/components/Box/Box.tsx b/packages/blade/src/components/Box/Box.tsx index be98fc497d8..1885e90fad4 100644 --- a/packages/blade/src/components/Box/Box.tsx +++ b/packages/blade/src/components/Box/Box.tsx @@ -256,6 +256,7 @@ const _Box: React.ForwardRefRenderFunction = (props, ref) const Box = assignWithoutSideEffects(React.forwardRef(_Box), { displayName: 'Box', + componentId: 'Box', }); export { Box, makeBoxProps }; diff --git a/packages/blade/src/components/Button/BaseButton/BaseButton.tsx b/packages/blade/src/components/Button/BaseButton/BaseButton.tsx index ab96da83239..0c7fa054118 100644 --- a/packages/blade/src/components/Button/BaseButton/BaseButton.tsx +++ b/packages/blade/src/components/Button/BaseButton/BaseButton.tsx @@ -168,7 +168,7 @@ const getTextColorToken = ({ const getProps = ({ buttonTypographyTokens, - children, + childrenString, isDisabled, size, theme, @@ -177,7 +177,7 @@ const getProps = ({ hasIcon, }: { buttonTypographyTokens: ButtonTypography; - children?: string; + childrenString?: string; isDisabled: boolean; hasIcon: boolean; theme: Theme; @@ -192,7 +192,7 @@ const getProps = ({ }); } - const isIconOnly = hasIcon && (!children || children?.trim().length === 0); + const isIconOnly = hasIcon && (!childrenString || childrenString?.trim().length === 0); const props: BaseButtonStyleProps = { iconSize: isIconOnly ? buttonIconOnlySizeToIconSizeMap[size] : buttonSizeToIconSizeMap[size], spinnerSize: buttonSizeToSpinnerSizeMap[size], @@ -201,7 +201,8 @@ const getProps = ({ minHeight: makeSize(buttonMinHeight[size]), height: isIconOnly ? buttonIconOnlyHeightWidth[size] : undefined, width: isIconOnly ? buttonIconOnlyHeightWidth[size] : undefined, - iconPadding: hasIcon && children?.trim() ? `spacing.${buttonIconPadding[size]}` : undefined, + iconPadding: + hasIcon && childrenString?.trim() ? `spacing.${buttonIconPadding[size]}` : undefined, iconColor: getTextColorToken({ property: 'icon', variant, @@ -224,7 +225,7 @@ const getProps = ({ buttonPaddingRight: isIconOnly ? makeSpace(0) : makeSpace(theme.spacing[buttonPadding[size].right]), - text: size === 'xsmall' ? children?.trim().toUpperCase() : children?.trim(), + text: size === 'xsmall' ? childrenString?.trim().toUpperCase() : childrenString?.trim(), defaultBackgroundColor: getIn( theme.colors, getBackgroundColorToken({ property: 'background', variant, color, state: 'default' }), @@ -328,6 +329,8 @@ const _BaseButton: React.ForwardRefRenderFunction ) : null} {text ? ( - - {text} - + isChildrenComponent ? ( + children + ) : ( + + {text} + + ) ) : null} {Icon && iconPosition == 'right' ? ( { const { isOpen, triggererRef, triggererWrapperRef, dropdownTriggerer, setIsOpen } = useDropdown(); @@ -42,7 +43,8 @@ const _DropdownOverlay = ({ const isMenu = dropdownTriggerer !== dropdownComponentIds.triggers.SelectInput && - dropdownTriggerer !== dropdownComponentIds.triggers.AutoComplete; + dropdownTriggerer !== dropdownComponentIds.triggers.AutoComplete && + referenceRef == undefined; const { refs, floatingStyles, context } = useFloating({ open: isOpen, @@ -53,7 +55,9 @@ const _DropdownOverlay = ({ // Input triggers have their ref on internal input element but we want width height of overall visible input hence wrapperRef is needed // We fallback to use `triggererRef` for triggers like button and link where wrapper is not needed // Checkout: https://github.com/razorpay/blade/pull/1559#discussion_r1305438920 - reference: triggererWrapperRef.current ?? triggererRef.current, + reference: (referenceRef?.current ?? + triggererWrapperRef.current ?? + triggererRef.current) as Element, }, middleware: [ offset({ diff --git a/packages/blade/src/components/Dropdown/dropdownComponentIds.ts b/packages/blade/src/components/Dropdown/dropdownComponentIds.ts index 8f33543011d..f3e621cea46 100644 --- a/packages/blade/src/components/Dropdown/dropdownComponentIds.ts +++ b/packages/blade/src/components/Dropdown/dropdownComponentIds.ts @@ -1,6 +1,7 @@ export const dropdownComponentIds = { DropdownOverlay: 'DropdownOverlay', Dropdown: 'Dropdown', + BaseBox: 'BaseBox', triggers: { SelectInput: 'SelectInput', DropdownButton: 'DropdownButton', diff --git a/packages/blade/src/components/Dropdown/types.ts b/packages/blade/src/components/Dropdown/types.ts index fd048260e26..c305c00f9ef 100644 --- a/packages/blade/src/components/Dropdown/types.ts +++ b/packages/blade/src/components/Dropdown/types.ts @@ -1,7 +1,7 @@ import type React from 'react'; import type { Placement } from '@floating-ui/react'; import type { StyledPropsBlade } from '~components/Box/styledProps'; -import type { TestID } from '~utils/types'; +import type { ContainerElementType, TestID } from '~utils/types'; import type { BoxProps } from '~components/Box'; type DropdownProps = { @@ -29,6 +29,13 @@ type DropdownOverlayProps = { */ zIndex?: number; width?: BoxProps['width']; + /** + * Reference to the element which triggers the DropdownOverlay + * + * This is used to position the DropdownOverlay relative to a specific element, + * for example in PhoneNumberInput the DropdownOverlay is positioned relative to the input element + */ + referenceRef?: React.MutableRefObject; /** * Sets the placement of the DropdownOverlay * diff --git a/packages/blade/src/components/Input/BaseInput/BaseInput.tsx b/packages/blade/src/components/Input/BaseInput/BaseInput.tsx index 546c9ad1ac9..ad40b628afe 100644 --- a/packages/blade/src/components/Input/BaseInput/BaseInput.tsx +++ b/packages/blade/src/components/Input/BaseInput/BaseInput.tsx @@ -157,7 +157,13 @@ type BaseInputCommonProps = FormInputLabelProps & * * eg: consumers can render a loader or they could render a clear button */ - interactionElement?: ReactNode; + trailingInteractionElement?: ReactNode; + /** + * Element to be rendered before prefix. This is decided by the component which is extending BaseInput + * + * eg: consumers can render a country selector or button + */ + leadingInteractionElement?: ReactNode; /** * Suffix symbol to be displayed at the end of the input field. If trailingIcon is provided it'll be placed before it */ @@ -766,7 +772,8 @@ const _BaseInput: React.ForwardRefRenderFunction => { - if (hasInteractionElement && (hasSuffix || hasTrailingIcon || hasTrailingButton)) { - return { - paddingRight: 'spacing.2', - }; +}): SpacingValueType => { + if (hasTrailingInteractionElement && (hasSuffix || hasTrailingIcon || hasTrailingButton)) { + return 'spacing.2'; } - if (hasInteractionElement && !hasSuffix && !hasTrailingIcon && !hasTrailingButton) { - return { - paddingRight: 'spacing.4', - }; + if (hasTrailingInteractionElement && !hasSuffix && !hasTrailingIcon && !hasTrailingButton) { + return 'spacing.4'; } - return { - paddingRight: 'spacing.0', - }; + if (hasLeadingInteractionElement) { + return 'spacing.3'; + } + + return 'spacing.0'; }; const getSuffixStyles = ({ @@ -155,14 +156,16 @@ const getTrailingIconStyles = ({ export const getInputVisualsToBeRendered = ({ leadingIcon, prefix, - interactionElement, + trailingInteractionElement, + leadingInteractionElement, suffix, trailingIcon, trailingButton, }: InputVisuals) => ({ hasLeadingIcon: Boolean(leadingIcon), hasPrefix: Boolean(prefix), - hasInteractionElement: Boolean(interactionElement), + hasTrailingInteractionElement: Boolean(trailingInteractionElement), + hasLeadingInteractionElement: Boolean(leadingInteractionElement), hasSuffix: Boolean(suffix), hasTrailingIcon: Boolean(trailingIcon), hasTrailingButton: Boolean(trailingButton), @@ -171,7 +174,8 @@ export const getInputVisualsToBeRendered = ({ export const BaseInputVisuals = ({ leadingIcon: LeadingIcon, prefix, - interactionElement, + trailingInteractionElement, + leadingInteractionElement, suffix, trailingIcon: TrailingIcon, isDisabled, @@ -182,23 +186,25 @@ export const BaseInputVisuals = ({ const { hasLeadingIcon, hasPrefix, - hasInteractionElement, hasSuffix, + hasTrailingInteractionElement, + hasLeadingInteractionElement, hasTrailingIcon, hasTrailingButton, } = getInputVisualsToBeRendered({ leadingIcon: LeadingIcon, prefix, - interactionElement, + leadingInteractionElement, + trailingInteractionElement, suffix, trailingIcon: TrailingIcon, trailingButton: TrailingButton, size, }); - const hasLeadingVisuals = hasLeadingIcon || hasPrefix; + const hasLeadingVisuals = hasLeadingInteractionElement || hasLeadingIcon || hasPrefix; const hasTrailingVisuals = - hasInteractionElement || hasSuffix || hasTrailingIcon || hasTrailingButton; + hasTrailingInteractionElement || hasSuffix || hasTrailingIcon || hasTrailingButton; if (__DEV__) { if (hasTrailingButton && !isValidAllowedChildren(TrailingButton, 'Link')) { @@ -212,6 +218,21 @@ export const BaseInputVisuals = ({ if (hasLeadingVisuals) { return ( + {hasLeadingInteractionElement ? ( + + {leadingInteractionElement} + + ) : null} {LeadingIcon ? ( - {hasInteractionElement ? ( + {hasTrailingInteractionElement ? ( - {interactionElement} + {trailingInteractionElement} ) : null} {hasSuffix ? ( @@ -271,14 +292,12 @@ export const BaseInputVisuals = ({ display="flex" {...getTrailingIconStyles({ hasTrailingIcon, hasTrailingButton })} > - { - - } + ) : null} {TrailingButton ? ( diff --git a/packages/blade/src/components/Input/BaseInput/StyledBaseInput.native.tsx b/packages/blade/src/components/Input/BaseInput/StyledBaseInput.native.tsx index 77351f32474..14c19e66cb6 100644 --- a/packages/blade/src/components/Input/BaseInput/StyledBaseInput.native.tsx +++ b/packages/blade/src/components/Input/BaseInput/StyledBaseInput.native.tsx @@ -126,7 +126,8 @@ const getRNInputStyles = ( validationState: props.validationState, leadingIcon: props.leadingIcon, prefix: props.prefix, - interactionElement: props.interactionElement, + trailingInteractionElement: props.trailingInteractionElement, + leadingInteractionElement: props.leadingInteractionElement, suffix: props.suffix, trailingIcon: props.trailingIcon, isTextArea: props.isTextArea, @@ -158,7 +159,8 @@ const StyledNativeBaseInput = styled.TextInput( validationState, leadingIcon, prefix, - interactionElement, + trailingInteractionElement, + leadingInteractionElement, suffix, trailingIcon, isTextArea, @@ -176,7 +178,8 @@ const StyledNativeBaseInput = styled.TextInput( validationState, leadingIcon, prefix, - interactionElement, + trailingInteractionElement, + leadingInteractionElement, suffix, trailingIcon, isTextArea, @@ -196,7 +199,8 @@ const StyledNativeBaseButton = styled.TouchableOpacity { - if (hasInteractionElement || hasSuffix || hasTrailingIcon) { + if (hasTrailingInteractionElement || hasSuffix || hasTrailingIcon) { return theme.spacing[3]; } return theme.spacing[baseInputPaddingTokens.right[size]]; @@ -160,7 +161,8 @@ export const getBaseInputStyles = ({ isDisabled, leadingIcon, prefix, - interactionElement, + trailingInteractionElement, + leadingInteractionElement, suffix, trailingIcon, textAlign, @@ -173,13 +175,14 @@ export const getBaseInputStyles = ({ const { hasLeadingIcon, hasPrefix, - hasInteractionElement, + hasTrailingInteractionElement, hasSuffix, hasTrailingIcon, } = getInputVisualsToBeRendered({ leadingIcon, prefix, - interactionElement, + trailingInteractionElement, + leadingInteractionElement, suffix, trailingIcon, size, @@ -215,7 +218,7 @@ export const getBaseInputStyles = ({ ), paddingRight: getRightPadding({ theme, - hasInteractionElement, + hasTrailingInteractionElement, hasSuffix, hasTrailingIcon, size, diff --git a/packages/blade/src/components/Input/BaseInput/types.ts b/packages/blade/src/components/Input/BaseInput/types.ts index 935b71e966f..0c703e78404 100644 --- a/packages/blade/src/components/Input/BaseInput/types.ts +++ b/packages/blade/src/components/Input/BaseInput/types.ts @@ -74,7 +74,8 @@ export type StyledBaseInputProps = { | 'validationState' | 'leadingIcon' | 'prefix' - | 'interactionElement' + | 'trailingInteractionElement' + | 'leadingInteractionElement' | 'suffix' | 'trailingIcon' | 'maxCharacters' diff --git a/packages/blade/src/components/Input/BaseInput/utils.ts b/packages/blade/src/components/Input/BaseInput/utils.ts new file mode 100644 index 00000000000..64da8e359b0 --- /dev/null +++ b/packages/blade/src/components/Input/BaseInput/utils.ts @@ -0,0 +1,94 @@ +import type { BaseInputProps } from './BaseInput'; + +type Type = Exclude; + +type TextInputKeyboardAndAutoComplete = Pick< + BaseInputProps, + 'keyboardType' | 'keyboardReturnKeyType' | 'autoCompleteSuggestionType' | 'autoCapitalize' +> & { + type: Type; +}; + +const getKeyboardAndAutocompleteProps = ({ + type = 'text', + keyboardReturnKeyType, + autoCompleteSuggestionType, + autoCapitalize, +}: TextInputKeyboardAndAutoComplete): TextInputKeyboardAndAutoComplete => { + const keyboardAndAutocompleteProps: TextInputKeyboardAndAutoComplete = { + type, + keyboardType: 'text', + keyboardReturnKeyType: 'default', + autoCompleteSuggestionType: 'none', + autoCapitalize, + }; + + const keyboardConfigMap = { + text: { + keyboardType: 'text', + keyboardReturnKeyType: 'default', + autoCompleteSuggestionType: 'none', + autoCapitalize: undefined, + }, + telephone: { + keyboardType: 'telephone', + keyboardReturnKeyType: 'done', + autoCompleteSuggestionType: 'telephone', + autoCapitalize: undefined, + }, + email: { + keyboardType: 'email', + keyboardReturnKeyType: 'done', + autoCompleteSuggestionType: 'email', + autoCapitalize: 'none', + }, + url: { + keyboardType: 'url', + keyboardReturnKeyType: 'go', + autoCompleteSuggestionType: 'none', + autoCapitalize: 'none', + }, + number: { + keyboardType: 'decimal', + keyboardReturnKeyType: 'done', + autoCompleteSuggestionType: 'none', + autoCapitalize: undefined, + }, + search: { + keyboardType: 'search', + keyboardReturnKeyType: 'search', + autoCompleteSuggestionType: 'none', + autoCapitalize: undefined, + }, + } as const; + + const keyboardConfig = keyboardConfigMap[type]; + + keyboardAndAutocompleteProps.keyboardType = keyboardConfig.keyboardType; + + keyboardAndAutocompleteProps.keyboardReturnKeyType = + keyboardReturnKeyType ?? keyboardConfig.keyboardReturnKeyType; + + keyboardAndAutocompleteProps.autoCompleteSuggestionType = + autoCompleteSuggestionType ?? keyboardConfig.autoCompleteSuggestionType; + + keyboardAndAutocompleteProps.autoCapitalize = keyboardConfig.autoCapitalize; + + if (type === 'number') { + /* the default keyboardType:numeric shows alphanumeric keyboard on iOS but number pad on android. making it type:text and keyboardType:decimal fixes this on all platforms. + * source: https://css-tricks.com/everything-you-ever-wanted-to-know-about-keyboardType/#aa-decimal + */ + keyboardAndAutocompleteProps.type = 'text'; + } + + if (type === 'search') { + /* when input type:search is provided at that time browser adds a weird close button which collides with our clear button and then we have 2 clear buttons + * source: https://github.com/razorpay/blade/issues/857#issue-1457367160 + */ + keyboardAndAutocompleteProps.type = 'text'; + } + + return keyboardAndAutocompleteProps; +}; + +export { getKeyboardAndAutocompleteProps }; diff --git a/packages/blade/src/components/Input/DropdownInputTriggers/BaseDropdownInputTrigger.tsx b/packages/blade/src/components/Input/DropdownInputTriggers/BaseDropdownInputTrigger.tsx index 8197affc4db..cd6b8f27f58 100644 --- a/packages/blade/src/components/Input/DropdownInputTriggers/BaseDropdownInputTrigger.tsx +++ b/packages/blade/src/components/Input/DropdownInputTriggers/BaseDropdownInputTrigger.tsx @@ -296,7 +296,7 @@ const _BaseDropdownInputTrigger = ( onChange={props.isSelectInput ? undefined : props.onInputValueChange} onKeyDown={props.onTriggerKeydown} size={props.size} - interactionElement={ + trailingInteractionElement={ isAutoCompleteInHeader ? null : ( { diff --git a/packages/blade/src/components/Input/DropdownInputTriggers/__tests__/__snapshots__/AutoComplete.native.test.tsx.snap b/packages/blade/src/components/Input/DropdownInputTriggers/__tests__/__snapshots__/AutoComplete.native.test.tsx.snap index 75e0622aa4e..9d5fa14479f 100644 --- a/packages/blade/src/components/Input/DropdownInputTriggers/__tests__/__snapshots__/AutoComplete.native.test.tsx.snap +++ b/packages/blade/src/components/Input/DropdownInputTriggers/__tests__/__snapshots__/AutoComplete.native.test.tsx.snap @@ -367,12 +367,6 @@ exports[` with should render AutoComplete 1`] = ` editable={true} hasTags={false} id="dropdown-7-trigger-1-input-2" - interactionElement={ - - } isDropdownTrigger={true} isFocused={false} isTextArea={false} @@ -418,6 +412,12 @@ exports[` with should render AutoComplete 1`] = ` } textAlign="left" textContentType="none" + trailingInteractionElement={ + + } value="" valueComponentType="text" /> diff --git a/packages/blade/src/components/Input/PasswordInput/PasswordInput.tsx b/packages/blade/src/components/Input/PasswordInput/PasswordInput.tsx index 1dd43246503..e9c9ab5cd5b 100644 --- a/packages/blade/src/components/Input/PasswordInput/PasswordInput.tsx +++ b/packages/blade/src/components/Input/PasswordInput/PasswordInput.tsx @@ -175,7 +175,7 @@ const _PasswordInput: React.ForwardRefRenderFunction should render 1`] = ` editable={true} hasTags={false} id="password-field-1-input-2" - interactionElement={ - - } isFocused={false} isTextArea={false} keyboardType="default" @@ -291,6 +283,14 @@ exports[` should render 1`] = ` }, ] } + trailingInteractionElement={ + + } valueComponentType="text" /> should render large size 1`] = ` editable={true} hasTags={false} id="password-field-13-input-14" - interactionElement={ - - } isFocused={false} isTextArea={false} keyboardType="default" @@ -761,6 +753,14 @@ exports[` should render large size 1`] = ` }, ] } + trailingInteractionElement={ + + } valueComponentType="text" /> { + throwBladeError({ + message: 'CountrySelector is not yet implemented for native', + moduleName: 'CountrySelector', + }); + + return <>; +}; + +export { CountrySelector }; diff --git a/packages/blade/src/components/Input/PhoneNumberInput/CountrySelector.web.tsx b/packages/blade/src/components/Input/PhoneNumberInput/CountrySelector.web.tsx new file mode 100644 index 00000000000..a534d0bd179 --- /dev/null +++ b/packages/blade/src/components/Input/PhoneNumberInput/CountrySelector.web.tsx @@ -0,0 +1,118 @@ +import type { CountryCodeType, getFlagsForAllCountries } from '@razorpay/i18nify-js'; +import { getDialCodeByCountryCode, getFlagOfCountry } from '@razorpay/i18nify-js'; +import React from 'react'; +import styled from 'styled-components'; +import { + ActionList, + ActionListItem, + ActionListItemAsset, + ActionListItemText, +} from '~components/ActionList'; +import { BottomSheet, BottomSheetBody, BottomSheetHeader } from '~components/BottomSheet'; +import type { DropdownOverlayProps } from '~components/Dropdown'; +import { Dropdown, DropdownButton, DropdownOverlay } from '~components/Dropdown'; +import { ChevronDownIcon, ChevronUpIcon } from '~components/Icons'; +import { useIsMobile } from '~utils/useIsMobile'; +import { size as sizes } from '~tokens/global'; +import { makeSize } from '~utils'; +import BaseBox from '~components/Box/BaseBox'; + +const countryNameFormatter = new Intl.DisplayNames(['en'], { type: 'region' }); + +type CountryData = { + code: CountryCodeType; + name: string; +}[]; +type CounterSelectorProps = { + selectedCountry: CountryCodeType; + inputWrapperRef: DropdownOverlayProps['referenceRef']; + countryData: CountryData; + onItemClick: (props: { name: string }) => void; + flags: ReturnType; + isDisabled?: boolean; + size: 'medium' | 'large'; +}; + +const CountryDropdownButtonWrapper = styled(BaseBox)(() => { + return { + '& > button': { + padding: '0', + width: '100%', + }, + }; +}); + +const CountrySelector = ({ + isDisabled, + selectedCountry, + inputWrapperRef, + countryData, + onItemClick, + flags, + size, +}: CounterSelectorProps): React.ReactElement => { + const [isDropdownOpen, setIsDropdownOpen] = React.useState(false); + const isMobile = useIsMobile(); + + const actionList = ( + + {countryData.map((country) => { + return ( + } + title={country.name} + value={country.code} + trailing={ + {getDialCodeByCountryCode(country.code)} + } + /> + ); + })} + + ); + + const flagSize = { + medium: makeSize(sizes[20]), + large: makeSize(sizes[24]), + } as const; + + return ( + + {/* TODO: Remove once CountrySelector's button sizing is fixed in figma */} + + + {/* @ts-expect-error */} + + + + {isMobile ? ( + + + {actionList} + + ) : ( + {actionList} + )} + + ); +}; + +export { CountrySelector, countryNameFormatter }; diff --git a/packages/blade/src/components/Input/PhoneNumberInput/PhoneNumberInput.native.tsx b/packages/blade/src/components/Input/PhoneNumberInput/PhoneNumberInput.native.tsx new file mode 100644 index 00000000000..9d7120ca424 --- /dev/null +++ b/packages/blade/src/components/Input/PhoneNumberInput/PhoneNumberInput.native.tsx @@ -0,0 +1,13 @@ +import type { PhoneNumberInputProps } from './types'; +import { throwBladeError } from '~utils/logger'; + +const PhoneNumberInput = (_props: PhoneNumberInputProps): React.ReactElement => { + throwBladeError({ + message: 'PhoneNumberInput is not yet implemented for native', + moduleName: 'PhoneNumberInput', + }); + + return <>; +}; + +export { PhoneNumberInput }; diff --git a/packages/blade/src/components/Input/PhoneNumberInput/PhoneNumberInput.stories.tsx b/packages/blade/src/components/Input/PhoneNumberInput/PhoneNumberInput.stories.tsx new file mode 100644 index 00000000000..59e8f670e9f --- /dev/null +++ b/packages/blade/src/components/Input/PhoneNumberInput/PhoneNumberInput.stories.tsx @@ -0,0 +1,453 @@ +import React from 'react'; +import type { StoryFn, Meta } from '@storybook/react'; +import type { CountryCodeType } from '@razorpay/i18nify-js'; +import { isValidPhoneNumber } from '@razorpay/i18nify-js'; +import { Title } from '@storybook/addon-docs'; +import type { PhoneNumberInputProps } from './types'; +import { PhoneNumberInput } from './PhoneNumberInput'; +import { Box } from '~components/Box'; +import { Code, Text } from '~components/Typography'; +import { PhoneIcon } from '~components/Icons'; +import StoryPageWrapper from '~utils/storybook/StoryPageWrapper'; +import { Sandbox } from '~utils/storybook/Sandbox'; +import { getStyledPropsArgTypes } from '~components/Box/BaseBox/storybookArgTypes'; +import iconMap from '~components/Icons/iconMap'; +import { Button } from '~components/Button'; + +const propsCategory = { + BASE_PROPS: 'Text Input Props', + LABEL_PROPS: 'Label Props', + VALIDATION_PROPS: 'Validation Props', + VISUAL_PROPS: 'Visual Props', + KEYBOARD_PROPS: 'Keyboard Props', +}; + +const meta: Meta = { + title: 'Components/Input/PhoneNumberInput', + component: PhoneNumberInput, + tags: ['autodocs'], + args: { + country: undefined, + defaultCountry: 'IN', + size: 'medium', + showDialCode: true, + showCountrySelector: true, + allowedCountries: undefined, + defaultValue: undefined, + trailingIcon: undefined, + leadingIcon: undefined, + accessibilityLabel: undefined, + }, + argTypes: { + country: { + table: { + category: propsCategory.BASE_PROPS, + }, + }, + defaultCountry: { + table: { + category: propsCategory.BASE_PROPS, + }, + }, + allowedCountries: { + table: { + category: propsCategory.BASE_PROPS, + }, + }, + showDialCode: { + table: { + category: propsCategory.BASE_PROPS, + }, + }, + showCountrySelector: { + table: { + category: propsCategory.BASE_PROPS, + }, + }, + defaultValue: { + table: { + category: propsCategory.BASE_PROPS, + }, + }, + testID: { + table: { + category: propsCategory.BASE_PROPS, + }, + }, + size: { + table: { + category: propsCategory.BASE_PROPS, + }, + }, + name: { + table: { + category: propsCategory.BASE_PROPS, + }, + }, + isDisabled: { + table: { + category: propsCategory.BASE_PROPS, + }, + }, + value: { + table: { + category: propsCategory.BASE_PROPS, + }, + }, + textAlign: { + table: { + category: propsCategory.BASE_PROPS, + }, + }, + autoFocus: { + table: { + category: propsCategory.BASE_PROPS, + }, + }, + onSubmit: { + control: { + disable: true, + }, + table: { + category: propsCategory.BASE_PROPS, + }, + }, + onClick: { + control: { + disable: true, + }, + table: { + category: propsCategory.BASE_PROPS, + }, + }, + onCountryChange: { + table: { + category: propsCategory.BASE_PROPS, + }, + }, + onChange: { + table: { + category: propsCategory.BASE_PROPS, + }, + }, + onFocus: { + control: { + disable: true, + }, + table: { + category: propsCategory.BASE_PROPS, + }, + }, + onBlur: { + control: { + disable: true, + }, + table: { + category: propsCategory.BASE_PROPS, + }, + }, + label: { + table: { + category: propsCategory.LABEL_PROPS, + }, + }, + accessibilityLabel: { + table: { + category: propsCategory.LABEL_PROPS, + }, + }, + labelPosition: { + table: { + category: propsCategory.LABEL_PROPS, + }, + }, + necessityIndicator: { + table: { + category: propsCategory.VALIDATION_PROPS, + }, + }, + isRequired: { + table: { + category: propsCategory.VALIDATION_PROPS, + }, + }, + validationState: { + table: { + category: propsCategory.VALIDATION_PROPS, + }, + }, + helpText: { + table: { + category: propsCategory.VALIDATION_PROPS, + }, + }, + errorText: { + table: { + category: propsCategory.VALIDATION_PROPS, + }, + }, + successText: { + table: { + category: propsCategory.VALIDATION_PROPS, + }, + }, + leadingIcon: { + name: 'leadingIcon', + type: 'select', + options: Object.keys(iconMap), + table: { + category: propsCategory.VISUAL_PROPS, + }, + }, + trailingIcon: { + name: 'trailingIcon', + type: 'select', + options: Object.keys(iconMap), + table: { + category: propsCategory.VISUAL_PROPS, + }, + }, + onClearButtonClick: { + table: { + category: propsCategory.VISUAL_PROPS, + }, + }, + keyboardReturnKeyType: { + table: { + category: propsCategory.KEYBOARD_PROPS, + }, + }, + autoCompleteSuggestionType: { + table: { + category: propsCategory.KEYBOARD_PROPS, + }, + }, + ...getStyledPropsArgTypes(), + }, + parameters: { + docs: { + page: () => ( + + Usage + + {` + import { PhoneNumberInput } from '@razorpay/blade/components'; + + function App(): React.ReactElement { + return ( + console.log(e)} + /> + ) + } + + export default App; + `} + + + ), + }, + }, +}; + +const PhoneNumberInputTemplate: StoryFn = ({ ...args }) => { + return ; +}; + +export const Default = PhoneNumberInputTemplate.bind({}); + +const CountriesToShowTemplate: StoryFn = ({ ...args }) => { + return ( + + + By setting the {`allowedCountries={['IN', 'MY']}`} prop, We can + only show two countries in the Country Selector + + + + ); +}; +export const CountriesToShow = CountriesToShowTemplate.bind({}); +CountriesToShow.args = { + allowedCountries: ['IN', 'MY'], +}; + +export const SizeLarge = PhoneNumberInputTemplate.bind({}); +SizeLarge.storyName = 'Size: Large'; +SizeLarge.args = { + size: 'large', +}; + +export const WithoutCountrySelector = PhoneNumberInputTemplate.bind({}); +WithoutCountrySelector.args = { + showCountrySelector: false, +}; + +export const WithoutDialCode = PhoneNumberInputTemplate.bind({}); +WithoutDialCode.args = { + showDialCode: false, +}; + +export const DefaultCountry = PhoneNumberInputTemplate.bind({}); +DefaultCountry.args = { + defaultCountry: 'MY', +}; + +export const WithHelpText = PhoneNumberInputTemplate.bind({}); +WithHelpText.args = { + helpText: 'Phone number is needed for sending you invoice', +}; + +export const WithErrorText = PhoneNumberInputTemplate.bind({}); +WithErrorText.args = { + validationState: 'error', + errorText: 'Phone number is invalid', +}; + +export const WithSuccessText = PhoneNumberInputTemplate.bind({}); +WithSuccessText.args = { + validationState: 'success', + successText: 'Phone number is valid', +}; + +export const WithoutLabel = PhoneNumberInputTemplate.bind({}); +WithoutLabel.args = { + label: undefined, + accessibilityLabel: 'Enter your phone number', +}; + +export const WithLeadingIcon = PhoneNumberInputTemplate.bind({}); +WithLeadingIcon.args = { + showCountrySelector: false, + leadingIcon: PhoneIcon, +}; + +const ControlledCountrySelectorTemplate: StoryFn = () => { + const [selectedCountry, setSelectedCountry] = React.useState('IN'); + + return ( + + + Selected country: {selectedCountry} + + { + console.log(country); + setSelectedCountry(country); + }} + /> + + ); +}; +export const ControlledCountrySelector = ControlledCountrySelectorTemplate.bind({}); + +const ControlledTemplate: StoryFn = () => { + const [inputValue, setInputValue] = React.useState(''); + const [data, setData] = React.useState<{ + phoneNumber: string; + dialCode: string; + country: string; + value: string; + name: string; + } | null>(null); + + return ( + + { + console.log(`sending ${name}:${value} to analytics service`); + setInputValue(value ?? ''); + setData({ + name, + value, + country, + dialCode, + phoneNumber, + }); + }} + /> + + {data ? ( + + + + value: + {' '} + {data.value} + + + + phoneNumber: + {' '} + {data.phoneNumber} + + + + country: + {' '} + {data.country} + + + + dialCode: + {' '} + {data.dialCode} + + + + name: + {' '} + {data.name} + + + ) : null} + + ); +}; +export const Controlled = ControlledTemplate.bind({}); + +const ValidationTemplate: StoryFn = () => { + const [inputValue, setInputValue] = React.useState(''); + const [isValid, setIsValid] = React.useState(true); + return ( + + + You can choose to validate the phone number manually by using the i18nify-js library's{' '} + isValidPhoneNumber() utility. + + { + setInputValue(value ?? ''); + setIsValid(isValidPhoneNumber(value, country)); + }} + /> + + ); +}; +export const Validation = ValidationTemplate.bind({}); + +export default meta; diff --git a/packages/blade/src/components/Input/PhoneNumberInput/PhoneNumberInput.web.tsx b/packages/blade/src/components/Input/PhoneNumberInput/PhoneNumberInput.web.tsx new file mode 100644 index 00000000000..b3192ed489a --- /dev/null +++ b/packages/blade/src/components/Input/PhoneNumberInput/PhoneNumberInput.web.tsx @@ -0,0 +1,233 @@ +/* eslint-disable @typescript-eslint/no-unnecessary-type-assertion */ +import type { CountryCodeType } from '@razorpay/i18nify-js'; +import { + formatPhoneNumber, + getDialCodeByCountryCode, + getFlagsForAllCountries, +} from '@razorpay/i18nify-js'; +import React from 'react'; +import type { PhoneNumberInputProps } from './types'; +import { countryNameFormatter, CountrySelector } from './CountrySelector'; +import { BaseInput } from '~components/Input/BaseInput'; +import { IconButton } from '~components/Button/IconButton'; +import isEmpty from '~utils/lodashButBetter/isEmpty'; +import { getKeyboardAndAutocompleteProps } from '~components/Input/BaseInput/utils'; +import { assignWithoutSideEffects } from '~utils/assignWithoutSideEffects'; +import { useMergeRefs } from '~utils/useMergeRefs'; +import type { BladeElementRef } from '~utils/types'; +import { CloseIcon } from '~components/Icons'; +import { MetaConstants } from '~utils/metaAttribute'; +import { useControllableState } from '~utils/useControllable'; + +const _PhoneNumberInput: React.ForwardRefRenderFunction = ( + { + defaultCountry = 'IN', + country, + onCountryChange, + label, + labelPosition, + defaultValue, + value, + name, + onChange, + necessityIndicator, + isRequired, + isDisabled, + leadingIcon, + trailingIcon, + validationState, + errorText, + helpText, + successText, + size = 'medium', + onClearButtonClick, + showCountrySelector = true, + showDialCode = true, + onClick, + onBlur, + onFocus, + accessibilityLabel = 'Enter phone number', + autoFocus, + testID, + keyboardReturnKeyType = 'done', + autoCompleteSuggestionType, + allowedCountries, + ...styledProps + }, + ref, +): React.ReactElement => { + const inputRef = React.useRef(null); + const mergedRef = useMergeRefs(ref, inputRef); + + const inputWrapperRef = React.useRef(null); + const [shouldShowClearButton, setShouldShowClearButton] = React.useState(false); + const [selectedCountry, setSelectedCountry] = useControllableState({ + defaultValue: defaultCountry as CountryCodeType, + value: country, + onChange: (country) => onCountryChange?.({ country }), + }); + + React.useEffect(() => { + setShouldShowClearButton(Boolean(defaultValue ?? value)); + }, [defaultValue, value]); + + const renderTrailingInteractionElement = (): React.ReactNode => { + if (!shouldShowClearButton) return null; + return ( + { + if (isEmpty(value) && inputRef.current) { + // when the input field is uncontrolled take the ref and clear the input and then call the onClearButtonClick function + if (inputRef.current instanceof HTMLInputElement) { + inputRef.current.value = ''; + inputRef.current.focus(); + } + } + // if the input field is controlled just call the click handler and the value change shall be left upto the consumer + onClearButtonClick?.(); + inputRef?.current?.focus(); + setShouldShowClearButton(false); + }} + isDisabled={isDisabled} + accessibilityLabel="Clear Input Content" + /> + ); + }; + + const flags = React.useMemo(() => getFlagsForAllCountries(), []); + const countryData = React.useMemo(() => { + if (allowedCountries) { + return allowedCountries.map((countryCode) => { + return { + code: countryCode, + name: countryNameFormatter.of(countryCode)!, + }; + }); + } + + return (Object.keys(flags) as CountryCodeType[]) + .map((countryCode) => { + return { + code: countryCode, + name: countryNameFormatter.of(countryCode)!, + }; + }) + .sort((a, b) => a.name.localeCompare(b.name)); + }, [allowedCountries, flags]); + + const handleOnChange = ({ + name, + value, + selectedCountry, + }: { + name?: string; + value?: string; + selectedCountry: CountryCodeType; + }): void => { + onChange?.({ + name: name!, + value: value!, + phoneNumber: value ? formatPhoneNumber(value, selectedCountry) : undefined!, + dialCode: getDialCodeByCountryCode(selectedCountry), + country: selectedCountry, + }); + }; + + const onItemClick = ({ name }: { name: string }): void => { + const selectedCountry = name as CountryCodeType; + setSelectedCountry(() => selectedCountry); + handleOnChange({ + selectedCountry, + name: inputRef.current?.name, + value: inputRef.current?.value, + }); + inputRef.current?.focus(); + }; + + return ( + { + inputWrapperRef.current = node as HTMLInputElement; + }} + ref={mergedRef} + id="phone-number-input" + componentName={MetaConstants.PhoneNumberInput} + label={label as string} + hideLabelText={!Boolean(label)} + labelPosition={labelPosition} + defaultValue={defaultValue} + value={value} + name={name} + placeholder={formatPhoneNumber('1234567890', selectedCountry)} + trailingIcon={trailingIcon} + leadingIcon={leadingIcon} + prefix={showDialCode ? getDialCodeByCountryCode(selectedCountry) : undefined} + trailingInteractionElement={renderTrailingInteractionElement()} + leadingInteractionElement={ + showCountrySelector ? ( + + ) : null + } + onChange={({ name, value }) => { + if (value?.length) { + // show the clear button when the user starts typing in + setShouldShowClearButton(true); + } + if (shouldShowClearButton && !value?.length) { + // hide the clear button when the input field is empty + setShouldShowClearButton(false); + } + handleOnChange({ name, value, selectedCountry }); + }} + onClick={onClick} + onFocus={onFocus} + onBlur={onBlur} + isDisabled={isDisabled} + accessibilityLabel={accessibilityLabel} + necessityIndicator={necessityIndicator} + isRequired={isRequired} + validationState={validationState} + errorText={errorText} + helpText={helpText} + successText={successText} + // eslint-disable-next-line jsx-a11y/no-autofocus + autoFocus={autoFocus} + testID={testID} + size={size} + {...getKeyboardAndAutocompleteProps({ + type: 'number', + keyboardReturnKeyType, + autoCompleteSuggestionType, + autoCapitalize: 'none', + })} + type="telephone" + {...styledProps} + /> + ); +}; + +/** + * PhoneNumberInput is a component that allows users to input phone numbers. + * It provides a country selector dropdown to select the country code. + * + * @example + * + * ```ts + * + * ``` + */ +const PhoneNumberInput = assignWithoutSideEffects(React.forwardRef(_PhoneNumberInput), { + displayName: 'PhoneNumberInput', +}); + +export { PhoneNumberInput }; diff --git a/packages/blade/src/components/Input/PhoneNumberInput/__tests__/PhoneNumberInput.ssr.test.tsx b/packages/blade/src/components/Input/PhoneNumberInput/__tests__/PhoneNumberInput.ssr.test.tsx new file mode 100644 index 00000000000..2a5ee689ff3 --- /dev/null +++ b/packages/blade/src/components/Input/PhoneNumberInput/__tests__/PhoneNumberInput.ssr.test.tsx @@ -0,0 +1,11 @@ +import { PhoneNumberInput } from '..'; +import renderWithSSR from '~utils/testing/renderWithSSR.web'; + +describe('', () => { + it('should render', () => { + const label = 'Enter phone number'; + const { container } = renderWithSSR(); + + expect(container).toMatchSnapshot(); + }); +}); diff --git a/packages/blade/src/components/Input/PhoneNumberInput/__tests__/PhoneNumberInput.test.stories.tsx b/packages/blade/src/components/Input/PhoneNumberInput/__tests__/PhoneNumberInput.test.stories.tsx new file mode 100644 index 00000000000..379934a4df3 --- /dev/null +++ b/packages/blade/src/components/Input/PhoneNumberInput/__tests__/PhoneNumberInput.test.stories.tsx @@ -0,0 +1,180 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable import/no-extraneous-dependencies */ +import type { StoryFn } from '@storybook/react'; +import { within, userEvent } from '@storybook/testing-library'; +import { expect, jest } from '@storybook/jest'; +import React from 'react'; +import { PhoneNumberInput } from '../PhoneNumberInput'; + +const sleep = (ms: number): Promise => new Promise((resolve) => setTimeout(resolve, ms)); + +const label = 'Phone Number'; +const onChangeFn = jest.fn(); +export const SelectACountry: StoryFn = (): React.ReactElement => { + return ; +}; + +SelectACountry.play = async () => { + const { getByLabelText, queryByRole, getByRole } = within(document.body); + + await sleep(100); + // Ensure the country selector is closed + await expect(queryByRole('menu')).not.toBeInTheDocument(); + + // Click on the country selector and open it + await userEvent.click(getByRole('button', { name: /select country/i })); + await sleep(300); + + // Use arrow keys to navigate to a country + await userEvent.keyboard('{arrowdown}'); + await userEvent.keyboard('{arrowdown}'); + + // Click on the country to select it + await userEvent.keyboard('{enter}'); + await sleep(300); + + // Ensure the country selector is closed + await expect(queryByRole('menu')).not.toBeInTheDocument(); + + // expect albania to be selected + await expect(getByRole('button', { name: /select country/i })).toHaveAccessibleName(/Albania/i); + + await sleep(300); + // Ensure that input is in focus, input is tel type; + await expect(getByLabelText(label)).toHaveFocus(); +}; + +export const UncontrolledState: StoryFn = (): React.ReactElement => { + return ( + { + onChangeFn(e); + }} + /> + ); +}; + +UncontrolledState.play = async () => { + onChangeFn.mockClear(); + const { getByLabelText } = within(document.body); + + await sleep(100); + const input = getByLabelText(label); + + // Focus on input + await userEvent.click(input); + + // Ensure default value is set + await expect(input).toHaveValue('9876543210'); + + // Type inside input + await userEvent.clear(input); + await userEvent.type(input, '1234567890'); + + // Ensure the value of the input updates + await expect(input).toHaveValue('1234567890'); + + await expect(onChangeFn).toHaveBeenLastCalledWith( + expect.objectContaining({ + country: 'IN', + dialCode: '+91', + name: undefined, + phoneNumber: '1234 567890', + value: '1234567890', + }), + ); +}; + +export const ControlledState: StoryFn = (): React.ReactElement => { + const [value, setValue] = React.useState('9876543210'); + return ( + { + onChangeFn(e); + setValue(e.value); + }} + /> + ); +}; + +ControlledState.play = async () => { + onChangeFn.mockClear(); + const { getByLabelText } = within(document.body); + + await sleep(100); + const input = getByLabelText(label); + + // Focus on input + await userEvent.click(input); + + // Ensure default value is set + await expect(input).toHaveValue('9876543210'); + + // Type inside input + await userEvent.clear(input); + await userEvent.type(input, '1234567890'); + + // Ensure the value of the input updates + await expect(input).toHaveValue('1234567890'); + + await expect(onChangeFn).toHaveBeenLastCalledWith( + expect.objectContaining({ + country: 'IN', + dialCode: '+91', + name: undefined, + phoneNumber: '1234 567890', + value: '1234567890', + }), + ); +}; + +export const Disabled: StoryFn = (): React.ReactElement => { + return ; +}; + +Disabled.play = async () => { + const { getByLabelText, getByRole } = within(document.body); + + await sleep(100); + const input = getByLabelText(label); + + // Ensure the input is disabled + await expect(input).toBeDisabled(); + // Ensure dropdown is disabled + await expect(getByRole('button', { name: /select country/i })).toBeDisabled(); + + // pressing tab should skip focus + await userEvent.tab(); + await expect(input).not.toHaveFocus(); + await userEvent.tab(); + await expect(getByRole('button', { name: /select country/i })).not.toHaveFocus(); +}; + +export const AutoFocus: StoryFn = (): React.ReactElement => { + // eslint-disable-next-line jsx-a11y/no-autofocus + return ; +}; + +AutoFocus.play = async () => { + const { getByLabelText } = within(document.body); + + await sleep(100); + // Ensure the input is focused + await expect(getByLabelText(label)).toHaveFocus(); +}; + +export default { + title: 'Components/Interaction Tests/PhoneNumberInput', + parameters: { + controls: { + disable: true, + }, + a11y: { disable: true }, + essentials: { disable: true }, + actions: { disable: true }, + }, +}; diff --git a/packages/blade/src/components/Input/PhoneNumberInput/__tests__/PhoneNumberInput.web.test.tsx b/packages/blade/src/components/Input/PhoneNumberInput/__tests__/PhoneNumberInput.web.test.tsx new file mode 100644 index 00000000000..da97d6b2a84 --- /dev/null +++ b/packages/blade/src/components/Input/PhoneNumberInput/__tests__/PhoneNumberInput.web.test.tsx @@ -0,0 +1,136 @@ +import userEvent from '@testing-library/user-event'; +import { useRef } from 'react'; +import { PhoneNumberInput } from '..'; +import renderWithTheme from '~utils/testing/renderWithTheme.web'; +import assertAccessible from '~utils/testing/assertAccessible.web'; +import { Button } from '~components/Button'; + +beforeAll(() => jest.spyOn(console, 'error').mockImplementation()); +afterAll(() => jest.restoreAllMocks()); + +describe('', () => { + it('should render', () => { + const { container } = renderWithTheme(); + + expect(container).toMatchSnapshot(); + }); + + it('should render large size', () => { + const { container } = renderWithTheme( + , + ); + + expect(container).toMatchSnapshot(); + }); + + it('should display success validation state', () => { + const label = 'Enter phone number'; + const { getByText, getByLabelText } = renderWithTheme( + , + ); + + const input = getByLabelText(label); + const successText = getByText('Success'); + + expect(successText).toBeTruthy(); + expect(input).toHaveAccessibleDescription('Success'); + expect(input).toBeValid(); + }); + + it('should display error validation state', () => { + const label = 'Enter phone number'; + const { getByText, getByLabelText } = renderWithTheme( + , + ); + + const input = getByLabelText(label); + const errorText = getByText('Error'); + + expect(errorText).toBeTruthy(); + expect(input).toHaveAccessibleDescription('Error'); + expect(input).toBeInvalid(); + }); + + it('should display help text', () => { + const label = 'Enter phone number'; + const { getByText, getByLabelText } = renderWithTheme( + , + ); + + const input = getByLabelText(label); + const HelpText = getByText('Help'); + + expect(HelpText).toBeTruthy(); + expect(input).toHaveAccessibleDescription('Help'); + expect(input).toBeValid(); + }); + + it.skip('should pass a11y', async () => { + const { getByLabelText } = renderWithTheme( + , + ); + + // A phone field can't be a textbox. There is currently no role for phone https://github.com/w3c/aria/issues/935 + const input = getByLabelText('Enter phone number'); + expect(input).toBeRequired(); + expect(input).toBeValid(); + expect(input).toBeEnabled(); + + await assertAccessible(input); + }); + + it(`should expose native element methods via ref`, async () => { + const label = 'Enter phone number'; + + const Example = (): React.ReactElement => { + const ref = useRef(null); + + return ( + <> + + + + ); + }; + const { getByLabelText, getByRole } = renderWithTheme(); + + const input = getByLabelText(label); + const button = getByRole('button', { name: 'Focus' }); + + expect(input).not.toHaveFocus(); + + await userEvent.click(button); + expect(input).toHaveFocus(); + }); + + it('should accept testID', () => { + const { getByTestId } = renderWithTheme( + , + ); + + expect(getByTestId('phone-input-test')).toBeTruthy(); + }); +}); diff --git a/packages/blade/src/components/Input/PhoneNumberInput/__tests__/__snapshots__/PhoneNumberInput.ssr.test.tsx.snap b/packages/blade/src/components/Input/PhoneNumberInput/__tests__/__snapshots__/PhoneNumberInput.ssr.test.tsx.snap new file mode 100644 index 00000000000..e76f0782caa --- /dev/null +++ b/packages/blade/src/components/Input/PhoneNumberInput/__tests__/__snapshots__/PhoneNumberInput.ssr.test.tsx.snap @@ -0,0 +1,12833 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` should render 1`] = `"

+91

"`; + +exports[` should render 2`] = ` +.c0.c0.c0.c0.c0 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + position: relative; + width: 100%; +} + +.c1.c1.c1.c1.c1 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + margin-bottom: 0px; + margin-top: 0px; +} + +.c2.c2.c2.c2.c2 { + margin-bottom: 4px; +} + +.c3.c3.c3.c3.c3 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-wrap: wrap; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + max-height: 36px; + gap: 0px; +} + +.c9.c9.c9.c9.c9 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-align-self: center; + -ms-flex-item-align: center; + align-self: center; +} + +.c10.c10.c10.c10.c10 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: stretch; + -webkit-box-align: stretch; + -ms-flex-align: stretch; + align-items: stretch; + -webkit-align-self: stretch; + -ms-flex-item-align: stretch; + align-self: stretch; + padding-left: 8px; +} + +.c11.c11.c11.c11.c11 { + text-align: left; + position: relative; +} + +.c12.c12.c12.c12.c12 { + margin-left: -3px; + width: 45px; +} + +.c16.c16.c16.c16.c16 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + z-index: 1; +} + +.c18.c18.c18.c18.c18 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + padding-left: 2px; +} + +.c19.c19.c19.c19.c19 { + display: none; + z-index: 1002; +} + +.c20.c20.c20.c20.c20 { + width: 100%; + box-shadow: 0px 8px 24px 0px hsla(217,56%,17%,0.12); +} + +.c24.c24.c24.c24.c24 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + max-height: 20px; +} + +.c25.c25.c25.c25.c25 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; +} + +.c26.c26.c26.c26.c26 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + padding-right: 8px; + padding-left: 8px; +} + +.c28.c28.c28.c28.c28 { + margin-left: auto; +} + +.c30.c30.c30.c30.c30 { + padding-left: 24px; +} + +.c31.c31.c31.c31.c31 { + padding-left: 12px; +} + +.c34.c34.c34.c34.c34 { + margin-left: 0px; +} + +.c35.c35.c35.c35.c35 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-box-pack: end; + -webkit-justify-content: flex-end; + -ms-flex-pack: end; + justify-content: flex-end; +} + +.c22.c22.c22.c22.c22 { + overflow-y: auto; + max-height: 300px; + padding: 8px; +} + +.c22.c22.c22.c22.c22 [role=group]:last-child > [role=separator]:last-child { + display: none; +} + +.c4.c4.c4.c4.c4 { + color: hsla(211,26%,34%,1); + font-family: "Inter","Inter Fallback Arial",Arial; + font-size: 0.75rem; + font-weight: 600; + font-style: normal; + -webkit-text-decoration-line: none; + text-decoration-line: none; + line-height: 1.125rem; + -webkit-letter-spacing: 0px; + -moz-letter-spacing: 0px; + -ms-letter-spacing: 0px; + letter-spacing: 0px; + margin: 0; + padding: 0; + overflow: hidden; + display: -webkit-box; + line-clamp: 2; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; +} + +.c6.c6.c6.c6.c6 { + color: hsla(212,39%,16%,1); + font-family: "Inter","Inter Fallback Arial",Arial; + font-size: 0.875rem; + font-weight: 400; + font-style: normal; + -webkit-text-decoration-line: none; + text-decoration-line: none; + line-height: 1.25rem; + -webkit-letter-spacing: 0px; + -moz-letter-spacing: 0px; + -ms-letter-spacing: 0px; + letter-spacing: 0px; + margin: 0; + padding: 0; +} + +.c27.c27.c27.c27.c27 { + color: hsla(212,39%,16%,1); + font-family: "Inter","Inter Fallback Arial",Arial; + font-size: 0.875rem; + font-weight: 400; + font-style: normal; + -webkit-text-decoration-line: none; + text-decoration-line: none; + line-height: 1.25rem; + -webkit-letter-spacing: 0px; + -moz-letter-spacing: 0px; + -ms-letter-spacing: 0px; + letter-spacing: 0px; + margin: 0; + padding: 0; + overflow: hidden; + display: -webkit-box; + line-clamp: 1; + -webkit-line-clamp: 1; + -webkit-box-orient: vertical; +} + +.c29.c29.c29.c29.c29 { + color: hsla(211,22%,56%,1); + font-family: "Inter","Inter Fallback Arial",Arial; + font-size: 0.875rem; + font-weight: 400; + font-style: italic; + -webkit-text-decoration-line: none; + text-decoration-line: none; + line-height: 1.25rem; + -webkit-letter-spacing: 0px; + -moz-letter-spacing: 0px; + -ms-letter-spacing: 0px; + letter-spacing: 0px; + margin: 0; + padding: 0; +} + +.c32.c32.c32.c32.c32 { + color: hsla(211,26%,34%,1); + font-family: "Inter","Inter Fallback Arial",Arial; + font-size: 0.875rem; + font-weight: 400; + font-style: normal; + -webkit-text-decoration-line: none; + text-decoration-line: none; + line-height: 1.25rem; + -webkit-letter-spacing: 0px; + -moz-letter-spacing: 0px; + -ms-letter-spacing: 0px; + letter-spacing: 0px; + margin: 0; + padding: 0; +} + +.c23.c23.c23.c23.c23 { + border-width: 4px; + border-style: solid; + border-color: transparent; + text-align: left; + background-color: transparent; + padding: 4px; + border-radius: 4px; + -webkit-text-decoration: none; + text-decoration: none; + cursor: pointer; + width: 100%; + display: block; +} + +.c23.c23.c23.c23.c23:hover:not([aria-disabled=true]) { + background-color: hsla(211,20%,52%,0.09); +} + +.c23.c23.c23.c23.c23[aria-selected=true] { + background-color: hsla(227,100%,59%,0.09); +} + +.c23.c23.c23.c23.c23[aria-selected=true]:hover { + background-color: hsla(227,100%,59%,0.18); +} + +.c5.c5.c5.c5.c5 { + border: 0; + -webkit-clip: rect(0 0 0 0); + clip: rect(0 0 0 0); + -webkit-clip-path: inset(50%); + clip-path: inset(50%); + height: 1px; + margin: 0 -1px -1px 0; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; + left: -10000px; + white-space: nowrap; + word-wrap: normal; +} + +.c21.c21.c21.c21.c21 { + background-color: hsla(0,0%,100%,1); + border-width: 1px; + border-color: hsla(211,20%,52%,0.18); + border-style: solid; + border-radius: 4px; +} + +.c14.c14.c14.c14.c14 { + min-height: 28px; + width: auto; + cursor: pointer; + background-color: hsla(211,20%,52%,0.09); + border-color: hsla(211,27%,76%,1); + border-width: 0px; + border-radius: 4px; + border-style: solid; + padding-top: 0px; + padding-bottom: 0px; + padding-left: 8px; + padding-right: 8px; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-text-decoration: none; + text-decoration: none; + overflow: hidden; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-transition-property: background-color,border-color,box-shadow; + transition-property: background-color,border-color,box-shadow; + -webkit-transition-timing-function: cubic-bezier(0.3,0,0.2,1); + transition-timing-function: cubic-bezier(0.3,0,0.2,1); + -webkit-transition-duration: 150ms; + transition-duration: 150ms; + position: relative; +} + +.c14.c14.c14.c14.c14:hover { + background-color: hsla(211,20%,52%,0.18); +} + +.c14.c14.c14.c14.c14:active { + background-color: hsla(211,20%,52%,0.18); +} + +.c14.c14.c14.c14.c14:focus-visible { + background-color: hsla(211,20%,52%,0.18); + outline: 1px solid hsla(227,100%,59%,0.09); + box-shadow: 0px 0px 0px 4px hsla(227,100%,59%,0.18); +} + +.c14.c14.c14.c14.c14 * { + -webkit-transition-property: color,fill,opacity; + transition-property: color,fill,opacity; + -webkit-transition-duration: 150ms; + transition-duration: 150ms; + -webkit-transition-timing-function: cubic-bezier(0.3,0,0.2,1); + transition-timing-function: cubic-bezier(0.3,0,0.2,1); +} + +.c15.c15.c15.c15.c15 { + -webkit-transform: scale(1); + -ms-transform: scale(1); + transform: scale(1); + -webkit-transition-duration: cubic-bezier(0.3,0,0.2,1); + transition-duration: cubic-bezier(0.3,0,0.2,1); + -webkit-transition-timing-function: 150px; + transition-timing-function: 150px; +} + +.c17.c17.c17.c17.c17 { + opacity: 1; +} + +.c13.c13.c13.c13.c13 > button { + padding: 0; + width: 100%; +} + +.c33.c33.c33.c33.c33 { + color: hsla(211,26%,34%,1); + font-family: "Inter","Inter Fallback Arial",Arial; + font-size: 0.875rem; + font-weight: 400; + font-style: normal; + -webkit-text-decoration-line: none; + text-decoration-line: none; + line-height: 1.25rem; + -webkit-letter-spacing: 0px; + -moz-letter-spacing: 0px; + -ms-letter-spacing: 0px; + letter-spacing: 0px; + margin: 0; + padding: 0; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + background-color: transparent; + padding-top: 8px; + padding-bottom: 8px; + padding-left: 8px; + padding-right: 12px; + width: 100%; + height: 36px; + min-height: 36px; + resize: none; + outline: none; + border: none; + cursor: auto; +} + +.c33.c33.c33.c33.c33::-webkit-input-placeholder { + color: hsla(211,20%,52%,0.32); + font-family: "Inter","Inter Fallback Arial",Arial; + font-size: 0.875rem; + font-weight: 400; + font-style: normal; + -webkit-text-decoration-line: none; + text-decoration-line: none; + line-height: 1.25rem; + -webkit-letter-spacing: 0px; + -moz-letter-spacing: 0px; + -ms-letter-spacing: 0px; + letter-spacing: 0px; + margin: 0; + padding: 0; +} + +.c33.c33.c33.c33.c33::-moz-placeholder { + color: hsla(211,20%,52%,0.32); + font-family: "Inter","Inter Fallback Arial",Arial; + font-size: 0.875rem; + font-weight: 400; + font-style: normal; + -webkit-text-decoration-line: none; + text-decoration-line: none; + line-height: 1.25rem; + -webkit-letter-spacing: 0px; + -moz-letter-spacing: 0px; + -ms-letter-spacing: 0px; + letter-spacing: 0px; + margin: 0; + padding: 0; +} + +.c33.c33.c33.c33.c33:-ms-input-placeholder { + color: hsla(211,20%,52%,0.32); + font-family: "Inter","Inter Fallback Arial",Arial; + font-size: 0.875rem; + font-weight: 400; + font-style: normal; + -webkit-text-decoration-line: none; + text-decoration-line: none; + line-height: 1.25rem; + -webkit-letter-spacing: 0px; + -moz-letter-spacing: 0px; + -ms-letter-spacing: 0px; + letter-spacing: 0px; + margin: 0; + padding: 0; +} + +.c33.c33.c33.c33.c33::placeholder { + color: hsla(211,20%,52%,0.32); + font-family: "Inter","Inter Fallback Arial",Arial; + font-size: 0.875rem; + font-weight: 400; + font-style: normal; + -webkit-text-decoration-line: none; + text-decoration-line: none; + line-height: 1.25rem; + -webkit-letter-spacing: 0px; + -moz-letter-spacing: 0px; + -ms-letter-spacing: 0px; + letter-spacing: 0px; + margin: 0; + padding: 0; +} + +.c33.c33.c33.c33.c33:focus { + outline: none; +} + +.c8.c8.c8.c8.c8 { + background-color: hsla(0,0%,100%,1); + border-radius: 4px; + border-style: solid; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + width: 100%; + position: relative; + border: none; + border-width: 0px; + box-shadow: hsla(211,27%,76%,1) 0px 0px 0px 1px; + -webkit-transition-property: box-shadow,background-color; + transition-property: box-shadow,background-color; + -webkit-transition-duration: 400ms; + transition-duration: 400ms; + -webkit-transition-easing: cubic-bezier(0.3,0,0.2,1); + transition-easing: cubic-bezier(0.3,0,0.2,1); +} + +.c8.c8.c8.c8.c8:hover { + background-color: hsla(210,40%,98%,1); + border-radius: 4px; + border-style: solid; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + width: 100%; + position: relative; + border: none; + border-width: 0px; + box-shadow: hsla(211,27%,76%,1) 0px 0px 0px 1px; + -webkit-transition-property: background-color; + transition-property: background-color; + -webkit-transition-duration: 150ms; + transition-duration: 150ms; + -webkit-transition-easing: cubic-bezier(0.3,0,0.2,1); + transition-easing: cubic-bezier(0.3,0,0.2,1); + -webkit-transition-timing-function: cubic-bezier(0.3,0,0.2,1); + transition-timing-function: cubic-bezier(0.3,0,0.2,1); +} + +.c8.c8.c8.c8.c8:focus-within { + background-color: hsla(0,0%,100%,1); + border-radius: 4px; + border-style: solid; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + width: 100%; + position: relative; + border: none; + border-width: 0px; + box-shadow: hsla(211,27%,76%,1) 0px 0px 0px 1px; + -webkit-transition-property: box-shadow,background-color; + transition-property: box-shadow,background-color; + -webkit-transition-duration: 400ms; + transition-duration: 400ms; + -webkit-transition-easing: cubic-bezier(0.3,0,0.2,1); + transition-easing: cubic-bezier(0.3,0,0.2,1); +} + +.c7.c7.c7.c7.c7 { + border-radius: 4px; + width: 100%; +} + +.c7.c7.c7.c7.c7:focus-within { + outline: 4px solid hsla(227,100%,59%,0.18); + outline-offset: 1px; + -webkit-transition-property: outline-width; + transition-property: outline-width; + -webkit-transition-duration: 400ms; + transition-duration: 400ms; + -webkit-transition-timing-function: cubic-bezier(0.3,0,0.2,1); + transition-timing-function: cubic-bezier(0.3,0,0.2,1); +} + +
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+ +
+
+
+
+
+
+

+ +91 +

+
+
+ +
+
+
+
+
+
+
+
+`; diff --git a/packages/blade/src/components/Input/PhoneNumberInput/__tests__/__snapshots__/PhoneNumberInput.web.test.tsx.snap b/packages/blade/src/components/Input/PhoneNumberInput/__tests__/__snapshots__/PhoneNumberInput.web.test.tsx.snap new file mode 100644 index 00000000000..fb5957d4c33 --- /dev/null +++ b/packages/blade/src/components/Input/PhoneNumberInput/__tests__/__snapshots__/PhoneNumberInput.web.test.tsx.snap @@ -0,0 +1,25657 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` should render 1`] = ` +.c0.c0.c0.c0.c0 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + position: relative; + width: 100%; +} + +.c1.c1.c1.c1.c1 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + margin-bottom: 0px; + margin-top: 0px; +} + +.c2.c2.c2.c2.c2 { + margin-bottom: 4px; +} + +.c3.c3.c3.c3.c3 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-wrap: wrap; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + max-height: 36px; + gap: 0px; +} + +.c9.c9.c9.c9.c9 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-align-self: center; + -ms-flex-item-align: center; + align-self: center; +} + +.c10.c10.c10.c10.c10 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: stretch; + -webkit-box-align: stretch; + -ms-flex-align: stretch; + align-items: stretch; + -webkit-align-self: stretch; + -ms-flex-item-align: stretch; + align-self: stretch; + padding-left: 8px; +} + +.c11.c11.c11.c11.c11 { + text-align: left; + position: relative; +} + +.c12.c12.c12.c12.c12 { + margin-left: -3px; + width: 45px; +} + +.c16.c16.c16.c16.c16 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + z-index: 1; +} + +.c18.c18.c18.c18.c18 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + padding-left: 2px; +} + +.c19.c19.c19.c19.c19 { + display: none; + z-index: 1002; +} + +.c20.c20.c20.c20.c20 { + width: 100%; + box-shadow: 0px 8px 24px 0px hsla(217,56%,17%,0.12); +} + +.c24.c24.c24.c24.c24 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + max-height: 20px; +} + +.c25.c25.c25.c25.c25 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; +} + +.c26.c26.c26.c26.c26 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + padding-right: 8px; + padding-left: 8px; +} + +.c28.c28.c28.c28.c28 { + margin-left: auto; +} + +.c30.c30.c30.c30.c30 { + padding-left: 24px; +} + +.c31.c31.c31.c31.c31 { + padding-left: 12px; +} + +.c34.c34.c34.c34.c34 { + margin-left: 0px; +} + +.c35.c35.c35.c35.c35 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-box-pack: end; + -webkit-justify-content: flex-end; + -ms-flex-pack: end; + justify-content: flex-end; +} + +.c22.c22.c22.c22.c22 { + overflow-y: auto; + max-height: 300px; + padding: 8px; +} + +.c22.c22.c22.c22.c22 [role=group]:last-child > [role=separator]:last-child { + display: none; +} + +.c4.c4.c4.c4.c4 { + color: hsla(211,26%,34%,1); + font-family: "Inter","Inter Fallback Arial",Arial; + font-size: 0.75rem; + font-weight: 600; + font-style: normal; + -webkit-text-decoration-line: none; + text-decoration-line: none; + line-height: 1.125rem; + -webkit-letter-spacing: 0px; + -moz-letter-spacing: 0px; + -ms-letter-spacing: 0px; + letter-spacing: 0px; + margin: 0; + padding: 0; + overflow: hidden; + display: -webkit-box; + line-clamp: 2; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; +} + +.c6.c6.c6.c6.c6 { + color: hsla(212,39%,16%,1); + font-family: "Inter","Inter Fallback Arial",Arial; + font-size: 0.875rem; + font-weight: 400; + font-style: normal; + -webkit-text-decoration-line: none; + text-decoration-line: none; + line-height: 1.25rem; + -webkit-letter-spacing: 0px; + -moz-letter-spacing: 0px; + -ms-letter-spacing: 0px; + letter-spacing: 0px; + margin: 0; + padding: 0; +} + +.c27.c27.c27.c27.c27 { + color: hsla(212,39%,16%,1); + font-family: "Inter","Inter Fallback Arial",Arial; + font-size: 0.875rem; + font-weight: 400; + font-style: normal; + -webkit-text-decoration-line: none; + text-decoration-line: none; + line-height: 1.25rem; + -webkit-letter-spacing: 0px; + -moz-letter-spacing: 0px; + -ms-letter-spacing: 0px; + letter-spacing: 0px; + margin: 0; + padding: 0; + overflow: hidden; + display: -webkit-box; + line-clamp: 1; + -webkit-line-clamp: 1; + -webkit-box-orient: vertical; +} + +.c29.c29.c29.c29.c29 { + color: hsla(211,22%,56%,1); + font-family: "Inter","Inter Fallback Arial",Arial; + font-size: 0.875rem; + font-weight: 400; + font-style: italic; + -webkit-text-decoration-line: none; + text-decoration-line: none; + line-height: 1.25rem; + -webkit-letter-spacing: 0px; + -moz-letter-spacing: 0px; + -ms-letter-spacing: 0px; + letter-spacing: 0px; + margin: 0; + padding: 0; +} + +.c32.c32.c32.c32.c32 { + color: hsla(211,26%,34%,1); + font-family: "Inter","Inter Fallback Arial",Arial; + font-size: 0.875rem; + font-weight: 400; + font-style: normal; + -webkit-text-decoration-line: none; + text-decoration-line: none; + line-height: 1.25rem; + -webkit-letter-spacing: 0px; + -moz-letter-spacing: 0px; + -ms-letter-spacing: 0px; + letter-spacing: 0px; + margin: 0; + padding: 0; +} + +.c23.c23.c23.c23.c23 { + border-width: 4px; + border-style: solid; + border-color: transparent; + text-align: left; + background-color: transparent; + padding: 4px; + border-radius: 4px; + -webkit-text-decoration: none; + text-decoration: none; + cursor: pointer; + width: 100%; + display: block; +} + +.c23.c23.c23.c23.c23:hover:not([aria-disabled=true]) { + background-color: hsla(211,20%,52%,0.09); +} + +.c23.c23.c23.c23.c23[aria-selected=true] { + background-color: hsla(227,100%,59%,0.09); +} + +.c23.c23.c23.c23.c23[aria-selected=true]:hover { + background-color: hsla(227,100%,59%,0.18); +} + +.c5.c5.c5.c5.c5 { + border: 0; + -webkit-clip: rect(0 0 0 0); + clip: rect(0 0 0 0); + -webkit-clip-path: inset(50%); + clip-path: inset(50%); + height: 1px; + margin: 0 -1px -1px 0; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; + left: -10000px; + white-space: nowrap; + word-wrap: normal; +} + +.c21.c21.c21.c21.c21 { + background-color: hsla(0,0%,100%,1); + border-width: 1px; + border-color: hsla(211,20%,52%,0.18); + border-style: solid; + border-radius: 4px; +} + +.c14.c14.c14.c14.c14 { + min-height: 28px; + width: auto; + cursor: pointer; + background-color: hsla(211,20%,52%,0.09); + border-color: hsla(211,27%,76%,1); + border-width: 0px; + border-radius: 4px; + border-style: solid; + padding-top: 0px; + padding-bottom: 0px; + padding-left: 8px; + padding-right: 8px; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-text-decoration: none; + text-decoration: none; + overflow: hidden; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-transition-property: background-color,border-color,box-shadow; + transition-property: background-color,border-color,box-shadow; + -webkit-transition-timing-function: cubic-bezier(0.3,0,0.2,1); + transition-timing-function: cubic-bezier(0.3,0,0.2,1); + -webkit-transition-duration: 150ms; + transition-duration: 150ms; + position: relative; +} + +.c14.c14.c14.c14.c14:hover { + background-color: hsla(211,20%,52%,0.18); +} + +.c14.c14.c14.c14.c14:active { + background-color: hsla(211,20%,52%,0.18); +} + +.c14.c14.c14.c14.c14:focus-visible { + background-color: hsla(211,20%,52%,0.18); + outline: 1px solid hsla(227,100%,59%,0.09); + box-shadow: 0px 0px 0px 4px hsla(227,100%,59%,0.18); +} + +.c14.c14.c14.c14.c14 * { + -webkit-transition-property: color,fill,opacity; + transition-property: color,fill,opacity; + -webkit-transition-duration: 150ms; + transition-duration: 150ms; + -webkit-transition-timing-function: cubic-bezier(0.3,0,0.2,1); + transition-timing-function: cubic-bezier(0.3,0,0.2,1); +} + +.c15.c15.c15.c15.c15 { + -webkit-transform: scale(1); + -ms-transform: scale(1); + transform: scale(1); + -webkit-transition-duration: cubic-bezier(0.3,0,0.2,1); + transition-duration: cubic-bezier(0.3,0,0.2,1); + -webkit-transition-timing-function: 150px; + transition-timing-function: 150px; +} + +.c17.c17.c17.c17.c17 { + opacity: 1; +} + +.c13.c13.c13.c13.c13 > button { + padding: 0; + width: 100%; +} + +.c33.c33.c33.c33.c33 { + color: hsla(211,26%,34%,1); + font-family: "Inter","Inter Fallback Arial",Arial; + font-size: 0.875rem; + font-weight: 400; + font-style: normal; + -webkit-text-decoration-line: none; + text-decoration-line: none; + line-height: 1.25rem; + -webkit-letter-spacing: 0px; + -moz-letter-spacing: 0px; + -ms-letter-spacing: 0px; + letter-spacing: 0px; + margin: 0; + padding: 0; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + background-color: transparent; + padding-top: 8px; + padding-bottom: 8px; + padding-left: 8px; + padding-right: 12px; + width: 100%; + height: 36px; + min-height: 36px; + resize: none; + outline: none; + border: none; + cursor: auto; +} + +.c33.c33.c33.c33.c33::-webkit-input-placeholder { + color: hsla(211,20%,52%,0.32); + font-family: "Inter","Inter Fallback Arial",Arial; + font-size: 0.875rem; + font-weight: 400; + font-style: normal; + -webkit-text-decoration-line: none; + text-decoration-line: none; + line-height: 1.25rem; + -webkit-letter-spacing: 0px; + -moz-letter-spacing: 0px; + -ms-letter-spacing: 0px; + letter-spacing: 0px; + margin: 0; + padding: 0; +} + +.c33.c33.c33.c33.c33::-moz-placeholder { + color: hsla(211,20%,52%,0.32); + font-family: "Inter","Inter Fallback Arial",Arial; + font-size: 0.875rem; + font-weight: 400; + font-style: normal; + -webkit-text-decoration-line: none; + text-decoration-line: none; + line-height: 1.25rem; + -webkit-letter-spacing: 0px; + -moz-letter-spacing: 0px; + -ms-letter-spacing: 0px; + letter-spacing: 0px; + margin: 0; + padding: 0; +} + +.c33.c33.c33.c33.c33:-ms-input-placeholder { + color: hsla(211,20%,52%,0.32); + font-family: "Inter","Inter Fallback Arial",Arial; + font-size: 0.875rem; + font-weight: 400; + font-style: normal; + -webkit-text-decoration-line: none; + text-decoration-line: none; + line-height: 1.25rem; + -webkit-letter-spacing: 0px; + -moz-letter-spacing: 0px; + -ms-letter-spacing: 0px; + letter-spacing: 0px; + margin: 0; + padding: 0; +} + +.c33.c33.c33.c33.c33::placeholder { + color: hsla(211,20%,52%,0.32); + font-family: "Inter","Inter Fallback Arial",Arial; + font-size: 0.875rem; + font-weight: 400; + font-style: normal; + -webkit-text-decoration-line: none; + text-decoration-line: none; + line-height: 1.25rem; + -webkit-letter-spacing: 0px; + -moz-letter-spacing: 0px; + -ms-letter-spacing: 0px; + letter-spacing: 0px; + margin: 0; + padding: 0; +} + +.c33.c33.c33.c33.c33:focus { + outline: none; +} + +.c8.c8.c8.c8.c8 { + background-color: hsla(0,0%,100%,1); + border-radius: 4px; + border-style: solid; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + width: 100%; + position: relative; + border: none; + border-width: 0px; + box-shadow: hsla(211,27%,76%,1) 0px 0px 0px 1px; + -webkit-transition-property: box-shadow,background-color; + transition-property: box-shadow,background-color; + -webkit-transition-duration: 400ms; + transition-duration: 400ms; + -webkit-transition-easing: cubic-bezier(0.3,0,0.2,1); + transition-easing: cubic-bezier(0.3,0,0.2,1); +} + +.c8.c8.c8.c8.c8:hover { + background-color: hsla(210,40%,98%,1); + border-radius: 4px; + border-style: solid; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + width: 100%; + position: relative; + border: none; + border-width: 0px; + box-shadow: hsla(211,27%,76%,1) 0px 0px 0px 1px; + -webkit-transition-property: background-color; + transition-property: background-color; + -webkit-transition-duration: 150ms; + transition-duration: 150ms; + -webkit-transition-easing: cubic-bezier(0.3,0,0.2,1); + transition-easing: cubic-bezier(0.3,0,0.2,1); + -webkit-transition-timing-function: cubic-bezier(0.3,0,0.2,1); + transition-timing-function: cubic-bezier(0.3,0,0.2,1); +} + +.c8.c8.c8.c8.c8:focus-within { + background-color: hsla(0,0%,100%,1); + border-radius: 4px; + border-style: solid; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + width: 100%; + position: relative; + border: none; + border-width: 0px; + box-shadow: hsla(211,27%,76%,1) 0px 0px 0px 1px; + -webkit-transition-property: box-shadow,background-color; + transition-property: box-shadow,background-color; + -webkit-transition-duration: 400ms; + transition-duration: 400ms; + -webkit-transition-easing: cubic-bezier(0.3,0,0.2,1); + transition-easing: cubic-bezier(0.3,0,0.2,1); +} + +.c7.c7.c7.c7.c7 { + border-radius: 4px; + width: 100%; +} + +.c7.c7.c7.c7.c7:focus-within { + outline: 4px solid hsla(227,100%,59%,0.18); + outline-offset: 1px; + -webkit-transition-property: outline-width; + transition-property: outline-width; + -webkit-transition-duration: 400ms; + transition-duration: 400ms; + -webkit-transition-timing-function: cubic-bezier(0.3,0,0.2,1); + transition-timing-function: cubic-bezier(0.3,0,0.2,1); +} + +
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+ +
+
+
+
+
+
+

+ +91 +

+
+
+ +
+
+
+
+
+
+
+
+`; + +exports[` should render large size 1`] = ` +.c0.c0.c0.c0.c0 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + position: relative; + width: 100%; +} + +.c1.c1.c1.c1.c1 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + margin-bottom: 0px; + margin-top: 0px; +} + +.c2.c2.c2.c2.c2 { + margin-bottom: 8px; +} + +.c3.c3.c3.c3.c3 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-wrap: wrap; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + max-height: 36px; + gap: 0px; +} + +.c9.c9.c9.c9.c9 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-align-self: center; + -ms-flex-item-align: center; + align-self: center; +} + +.c10.c10.c10.c10.c10 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: stretch; + -webkit-box-align: stretch; + -ms-flex-align: stretch; + align-items: stretch; + -webkit-align-self: stretch; + -ms-flex-item-align: stretch; + align-self: stretch; + padding-left: 8px; +} + +.c11.c11.c11.c11.c11 { + text-align: left; + position: relative; +} + +.c12.c12.c12.c12.c12 { + margin-left: -2px; + width: 60px; +} + +.c16.c16.c16.c16.c16 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + z-index: 1; +} + +.c18.c18.c18.c18.c18 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + padding-left: 8px; +} + +.c19.c19.c19.c19.c19 { + display: none; + z-index: 1002; +} + +.c20.c20.c20.c20.c20 { + width: 100%; + box-shadow: 0px 8px 24px 0px hsla(217,56%,17%,0.12); +} + +.c24.c24.c24.c24.c24 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + max-height: 20px; +} + +.c25.c25.c25.c25.c25 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; +} + +.c26.c26.c26.c26.c26 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + padding-right: 8px; + padding-left: 8px; +} + +.c28.c28.c28.c28.c28 { + margin-left: auto; +} + +.c30.c30.c30.c30.c30 { + padding-left: 24px; +} + +.c31.c31.c31.c31.c31 { + padding-left: 12px; +} + +.c34.c34.c34.c34.c34 { + margin-left: 0px; +} + +.c35.c35.c35.c35.c35 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-box-pack: end; + -webkit-justify-content: flex-end; + -ms-flex-pack: end; + justify-content: flex-end; +} + +.c22.c22.c22.c22.c22 { + overflow-y: auto; + max-height: 300px; + padding: 8px; +} + +.c22.c22.c22.c22.c22 [role=group]:last-child > [role=separator]:last-child { + display: none; +} + +.c4.c4.c4.c4.c4 { + color: hsla(211,26%,34%,1); + font-family: "Inter","Inter Fallback Arial",Arial; + font-size: 0.875rem; + font-weight: 600; + font-style: normal; + -webkit-text-decoration-line: none; + text-decoration-line: none; + line-height: 1.25rem; + -webkit-letter-spacing: 0px; + -moz-letter-spacing: 0px; + -ms-letter-spacing: 0px; + letter-spacing: 0px; + margin: 0; + padding: 0; + overflow: hidden; + display: -webkit-box; + line-clamp: 2; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; +} + +.c6.c6.c6.c6.c6 { + color: hsla(212,39%,16%,1); + font-family: "Inter","Inter Fallback Arial",Arial; + font-size: 0.875rem; + font-weight: 400; + font-style: normal; + -webkit-text-decoration-line: none; + text-decoration-line: none; + line-height: 1.25rem; + -webkit-letter-spacing: 0px; + -moz-letter-spacing: 0px; + -ms-letter-spacing: 0px; + letter-spacing: 0px; + margin: 0; + padding: 0; +} + +.c27.c27.c27.c27.c27 { + color: hsla(212,39%,16%,1); + font-family: "Inter","Inter Fallback Arial",Arial; + font-size: 0.875rem; + font-weight: 400; + font-style: normal; + -webkit-text-decoration-line: none; + text-decoration-line: none; + line-height: 1.25rem; + -webkit-letter-spacing: 0px; + -moz-letter-spacing: 0px; + -ms-letter-spacing: 0px; + letter-spacing: 0px; + margin: 0; + padding: 0; + overflow: hidden; + display: -webkit-box; + line-clamp: 1; + -webkit-line-clamp: 1; + -webkit-box-orient: vertical; +} + +.c29.c29.c29.c29.c29 { + color: hsla(211,22%,56%,1); + font-family: "Inter","Inter Fallback Arial",Arial; + font-size: 0.875rem; + font-weight: 400; + font-style: italic; + -webkit-text-decoration-line: none; + text-decoration-line: none; + line-height: 1.25rem; + -webkit-letter-spacing: 0px; + -moz-letter-spacing: 0px; + -ms-letter-spacing: 0px; + letter-spacing: 0px; + margin: 0; + padding: 0; +} + +.c32.c32.c32.c32.c32 { + color: hsla(211,26%,34%,1); + font-family: "Inter","Inter Fallback Arial",Arial; + font-size: 1rem; + font-weight: 400; + font-style: normal; + -webkit-text-decoration-line: none; + text-decoration-line: none; + line-height: 1.5rem; + -webkit-letter-spacing: 0px; + -moz-letter-spacing: 0px; + -ms-letter-spacing: 0px; + letter-spacing: 0px; + margin: 0; + padding: 0; +} + +.c23.c23.c23.c23.c23 { + border-width: 4px; + border-style: solid; + border-color: transparent; + text-align: left; + background-color: transparent; + padding: 4px; + border-radius: 4px; + -webkit-text-decoration: none; + text-decoration: none; + cursor: pointer; + width: 100%; + display: block; +} + +.c23.c23.c23.c23.c23:hover:not([aria-disabled=true]) { + background-color: hsla(211,20%,52%,0.09); +} + +.c23.c23.c23.c23.c23[aria-selected=true] { + background-color: hsla(227,100%,59%,0.09); +} + +.c23.c23.c23.c23.c23[aria-selected=true]:hover { + background-color: hsla(227,100%,59%,0.18); +} + +.c5.c5.c5.c5.c5 { + border: 0; + -webkit-clip: rect(0 0 0 0); + clip: rect(0 0 0 0); + -webkit-clip-path: inset(50%); + clip-path: inset(50%); + height: 1px; + margin: 0 -1px -1px 0; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; + left: -10000px; + white-space: nowrap; + word-wrap: normal; +} + +.c21.c21.c21.c21.c21 { + background-color: hsla(0,0%,100%,1); + border-width: 1px; + border-color: hsla(211,20%,52%,0.18); + border-style: solid; + border-radius: 4px; +} + +.c14.c14.c14.c14.c14 { + min-height: 36px; + width: auto; + cursor: pointer; + background-color: hsla(211,20%,52%,0.09); + border-color: hsla(211,27%,76%,1); + border-width: 0px; + border-radius: 4px; + border-style: solid; + padding-top: 0px; + padding-bottom: 0px; + padding-left: 20px; + padding-right: 20px; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-text-decoration: none; + text-decoration: none; + overflow: hidden; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-transition-property: background-color,border-color,box-shadow; + transition-property: background-color,border-color,box-shadow; + -webkit-transition-timing-function: cubic-bezier(0.3,0,0.2,1); + transition-timing-function: cubic-bezier(0.3,0,0.2,1); + -webkit-transition-duration: 150ms; + transition-duration: 150ms; + position: relative; +} + +.c14.c14.c14.c14.c14:hover { + background-color: hsla(211,20%,52%,0.18); +} + +.c14.c14.c14.c14.c14:active { + background-color: hsla(211,20%,52%,0.18); +} + +.c14.c14.c14.c14.c14:focus-visible { + background-color: hsla(211,20%,52%,0.18); + outline: 1px solid hsla(227,100%,59%,0.09); + box-shadow: 0px 0px 0px 4px hsla(227,100%,59%,0.18); +} + +.c14.c14.c14.c14.c14 * { + -webkit-transition-property: color,fill,opacity; + transition-property: color,fill,opacity; + -webkit-transition-duration: 150ms; + transition-duration: 150ms; + -webkit-transition-timing-function: cubic-bezier(0.3,0,0.2,1); + transition-timing-function: cubic-bezier(0.3,0,0.2,1); +} + +.c15.c15.c15.c15.c15 { + -webkit-transform: scale(1); + -ms-transform: scale(1); + transform: scale(1); + -webkit-transition-duration: cubic-bezier(0.3,0,0.2,1); + transition-duration: cubic-bezier(0.3,0,0.2,1); + -webkit-transition-timing-function: 150px; + transition-timing-function: 150px; +} + +.c17.c17.c17.c17.c17 { + opacity: 1; +} + +.c13.c13.c13.c13.c13 > button { + padding: 0; + width: 100%; +} + +.c33.c33.c33.c33.c33 { + color: hsla(211,26%,34%,1); + font-family: "Inter","Inter Fallback Arial",Arial; + font-size: 1rem; + font-weight: 400; + font-style: normal; + -webkit-text-decoration-line: none; + text-decoration-line: none; + line-height: 1.5rem; + -webkit-letter-spacing: 0px; + -moz-letter-spacing: 0px; + -ms-letter-spacing: 0px; + letter-spacing: 0px; + margin: 0; + padding: 0; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + background-color: transparent; + padding-top: 12px; + padding-bottom: 12px; + padding-left: 8px; + padding-right: 12px; + width: 100%; + height: 48px; + min-height: 48px; + resize: none; + outline: none; + border: none; + cursor: auto; +} + +.c33.c33.c33.c33.c33::-webkit-input-placeholder { + color: hsla(211,20%,52%,0.32); + font-family: "Inter","Inter Fallback Arial",Arial; + font-size: 1rem; + font-weight: 400; + font-style: normal; + -webkit-text-decoration-line: none; + text-decoration-line: none; + line-height: 1.5rem; + -webkit-letter-spacing: 0px; + -moz-letter-spacing: 0px; + -ms-letter-spacing: 0px; + letter-spacing: 0px; + margin: 0; + padding: 0; +} + +.c33.c33.c33.c33.c33::-moz-placeholder { + color: hsla(211,20%,52%,0.32); + font-family: "Inter","Inter Fallback Arial",Arial; + font-size: 1rem; + font-weight: 400; + font-style: normal; + -webkit-text-decoration-line: none; + text-decoration-line: none; + line-height: 1.5rem; + -webkit-letter-spacing: 0px; + -moz-letter-spacing: 0px; + -ms-letter-spacing: 0px; + letter-spacing: 0px; + margin: 0; + padding: 0; +} + +.c33.c33.c33.c33.c33:-ms-input-placeholder { + color: hsla(211,20%,52%,0.32); + font-family: "Inter","Inter Fallback Arial",Arial; + font-size: 1rem; + font-weight: 400; + font-style: normal; + -webkit-text-decoration-line: none; + text-decoration-line: none; + line-height: 1.5rem; + -webkit-letter-spacing: 0px; + -moz-letter-spacing: 0px; + -ms-letter-spacing: 0px; + letter-spacing: 0px; + margin: 0; + padding: 0; +} + +.c33.c33.c33.c33.c33::placeholder { + color: hsla(211,20%,52%,0.32); + font-family: "Inter","Inter Fallback Arial",Arial; + font-size: 1rem; + font-weight: 400; + font-style: normal; + -webkit-text-decoration-line: none; + text-decoration-line: none; + line-height: 1.5rem; + -webkit-letter-spacing: 0px; + -moz-letter-spacing: 0px; + -ms-letter-spacing: 0px; + letter-spacing: 0px; + margin: 0; + padding: 0; +} + +.c33.c33.c33.c33.c33:focus { + outline: none; +} + +.c8.c8.c8.c8.c8 { + background-color: hsla(0,0%,100%,1); + border-radius: 4px; + border-style: solid; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + width: 100%; + position: relative; + border: none; + border-width: 0px; + box-shadow: hsla(211,27%,76%,1) 0px 0px 0px 1px; + -webkit-transition-property: box-shadow,background-color; + transition-property: box-shadow,background-color; + -webkit-transition-duration: 400ms; + transition-duration: 400ms; + -webkit-transition-easing: cubic-bezier(0.3,0,0.2,1); + transition-easing: cubic-bezier(0.3,0,0.2,1); +} + +.c8.c8.c8.c8.c8:hover { + background-color: hsla(210,40%,98%,1); + border-radius: 4px; + border-style: solid; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + width: 100%; + position: relative; + border: none; + border-width: 0px; + box-shadow: hsla(211,27%,76%,1) 0px 0px 0px 1px; + -webkit-transition-property: background-color; + transition-property: background-color; + -webkit-transition-duration: 150ms; + transition-duration: 150ms; + -webkit-transition-easing: cubic-bezier(0.3,0,0.2,1); + transition-easing: cubic-bezier(0.3,0,0.2,1); + -webkit-transition-timing-function: cubic-bezier(0.3,0,0.2,1); + transition-timing-function: cubic-bezier(0.3,0,0.2,1); +} + +.c8.c8.c8.c8.c8:focus-within { + background-color: hsla(0,0%,100%,1); + border-radius: 4px; + border-style: solid; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + width: 100%; + position: relative; + border: none; + border-width: 0px; + box-shadow: hsla(211,27%,76%,1) 0px 0px 0px 1px; + -webkit-transition-property: box-shadow,background-color; + transition-property: box-shadow,background-color; + -webkit-transition-duration: 400ms; + transition-duration: 400ms; + -webkit-transition-easing: cubic-bezier(0.3,0,0.2,1); + transition-easing: cubic-bezier(0.3,0,0.2,1); +} + +.c7.c7.c7.c7.c7 { + border-radius: 4px; + width: 100%; +} + +.c7.c7.c7.c7.c7:focus-within { + outline: 4px solid hsla(227,100%,59%,0.18); + outline-offset: 1px; + -webkit-transition-property: outline-width; + transition-property: outline-width; + -webkit-transition-duration: 400ms; + transition-duration: 400ms; + -webkit-transition-timing-function: cubic-bezier(0.3,0,0.2,1); + transition-timing-function: cubic-bezier(0.3,0,0.2,1); +} + +
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+ +
+
+
+
+
+
+

+ +91 +

+
+
+ +
+
+
+
+
+
+
+
+`; diff --git a/packages/blade/src/components/Input/PhoneNumberInput/index.ts b/packages/blade/src/components/Input/PhoneNumberInput/index.ts new file mode 100644 index 00000000000..2663fc98594 --- /dev/null +++ b/packages/blade/src/components/Input/PhoneNumberInput/index.ts @@ -0,0 +1,2 @@ +export * from './PhoneNumberInput'; +export type { PhoneNumberInputProps } from './types'; diff --git a/packages/blade/src/components/Input/PhoneNumberInput/types.ts b/packages/blade/src/components/Input/PhoneNumberInput/types.ts new file mode 100644 index 00000000000..aacc1770855 --- /dev/null +++ b/packages/blade/src/components/Input/PhoneNumberInput/types.ts @@ -0,0 +1,109 @@ +import type { CountryCodeType } from '@razorpay/i18nify-js'; +import type { StyledPropsBlade } from '~components/Box/styledProps'; +import type { BaseInputProps } from '~components/Input/BaseInput'; + +type CommonProps = Pick< + BaseInputProps, + | 'size' + | 'label' + | 'labelPosition' + | 'name' + | 'validationState' + | 'errorText' + | 'successText' + | 'helpText' + | 'defaultValue' + | 'necessityIndicator' + | 'isRequired' + | 'isDisabled' + | 'onFocus' + | 'onBlur' + | 'onClick' + | 'leadingIcon' + | 'trailingIcon' + | 'accessibilityLabel' + | 'autoFocus' + | 'testID' + | 'keyboardReturnKeyType' + | 'autoCompleteSuggestionType' +>; + +type PhoneNumberInputProps = StyledPropsBlade & + CommonProps & { + /** + * Default value of the input, Used to set the default value of input field when it's uncontrolled + */ + defaultValue?: string; + /** + * Value of the input, Used to turn the input field to controlled so user can control the value + */ + value?: string; + /** + * The default country code to be used in the input. + * Uncontrolled state of the country code. + * + * @default "IN" or autodetect based on the user's locale + */ + defaultCountry?: CountryCodeType; + /** + * Controlled state of the country code to be used in the input. + */ + country?: CountryCodeType; + /** + * Callback that is called when the country is selected from the country selector. + */ + onCountryChange?: ({ country }: { country: CountryCodeType }) => void; + /** + * List of countries to be shown in the country selector. + */ + allowedCountries?: CountryCodeType[]; + /** + * Callback that is called when the value of the input changes. + */ + onChange?: (event: { + /** + * formatted phone number with dial code + * + * @example: "+91 123456789" + */ + phoneNumber: string; + /** + * dial code of the country + * + * @example: 91 for India + */ + dialCode: string; + /** + * country code of the country + * + * @example: "IN" for India + */ + country: CountryCodeType; + /** + * raw value of the input + */ + value: string; + /** + * name of the input + */ + name: string; + }) => void; + /** + * If true, the dial code text will be shown in the leading text. + * + * @default true + */ + showDialCode?: boolean; + /** + * If true, the country selector will be shown. + * + * @default true + */ + showCountrySelector?: boolean; + /** + * Callback that is called when the clear button is clicked. + */ + onClearButtonClick?: () => void; + }; + +export type { PhoneNumberInputProps }; diff --git a/packages/blade/src/components/Input/TextArea/TextArea.tsx b/packages/blade/src/components/Input/TextArea/TextArea.tsx index 12670d4798d..223524aa8c4 100644 --- a/packages/blade/src/components/Input/TextArea/TextArea.tsx +++ b/packages/blade/src/components/Input/TextArea/TextArea.tsx @@ -180,7 +180,7 @@ const _TextArea: React.ForwardRefRenderFunction name={name} maxCharacters={maxCharacters} placeholder={placeholder} - interactionElement={renderInteractionElement()} + trailingInteractionElement={renderInteractionElement()} defaultValue={defaultValue} value={value} numberOfLines={numberOfLines} diff --git a/packages/blade/src/components/Input/TextArea/__tests__/__snapshots__/TextArea.native.test.tsx.snap b/packages/blade/src/components/Input/TextArea/__tests__/__snapshots__/TextArea.native.test.tsx.snap index 12d2475c7a1..7947bdffd95 100644 --- a/packages/blade/src/components/Input/TextArea/__tests__/__snapshots__/TextArea.native.test.tsx.snap +++ b/packages/blade/src/components/Input/TextArea/__tests__/__snapshots__/TextArea.native.test.tsx.snap @@ -241,7 +241,6 @@ exports[`