Skip to content

Commit

Permalink
feat: added pin-input unstyled
Browse files Browse the repository at this point in the history
  • Loading branch information
akash3gtm committed Dec 23, 2024
1 parent 685d775 commit 984ea17
Show file tree
Hide file tree
Showing 11 changed files with 587 additions and 0 deletions.
20 changes: 20 additions & 0 deletions packages/unstyled/pin-input/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Dotfiles
.babelrc
.eslintignore
.eslintrc.json
.gitattributes
_config.yml
.editorconfig


#Config files
babel.config.js

# Documents
CONTRIBUTING.md
ISSUE_TEMPLATE.txt
img

# Test cases
__tests__
dist/__tests__
13 changes: 13 additions & 0 deletions packages/unstyled/pin-input/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# @gluestack-ui/pin-input

## 0.0.2

### Patch Changes

- V 0.0.2

## 0.0.1

### Patch Changes

- initial release
61 changes: 61 additions & 0 deletions packages/unstyled/pin-input/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# @gluestack-style/input

## Installation

To use `@gluestack-ui/input`, all you need to do is install the
`@gluestack-ui/input` package:

```sh
$ yarn add @gluestack-ui/pin-input

# or

$ npm i @gluestack-ui/pin-input
```

## Usage

The Input component is your go-to tool for gathering user input in a sleek and user-friendly text field. Whether you're designing a simple login form or a complex search feature, this component has got you covered. Here's an example how to use this package to create one:

```jsx
import { Root, Input } from '../components/core/input/styled-components';
import { createInput } from '@gluestack-ui/input';
const InputField = createInput({
Root,
Input,
});
```

## Customizing the input:

Default styling of all these components can be found in the components/core/input file. For reference, you can view the [source code](https://github.com/gluestack/gluestack-ui/blob/development/example/storybook/src/ui-components/Input/index.tsx) of the styled `input` components.

```jsx
// import the styles
import { Root, Input } from '../components/core/input/styled-components';

// import the createInput function
import { createInput } from '@gluestack-ui/input';

//import any icon
import { searchIcon } from '@gluestack/icons';

// Understanding the API
const InputField = createInput({
Root,
Input,
});

// Using the input component
export default () => (
<Input>
<InputSlot pl="$3">
<InputIcon as={SearchIcon} />
</InputSlot>
<InputInput placeholder="your text goes here..." />
</Input>
);
```

More guides on how to get started are available
[here](https://ui.gluestack.io/docs/components/forms/input).
29 changes: 29 additions & 0 deletions packages/unstyled/pin-input/babel.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
const path = require('path');

module.exports = function (api) {
api.cache(true);
return {
presets: ['babel-preset-expo'],
plugins: [
process.env.NODE_ENV !== 'production'
? [
'module-resolver',
{
alias: {
['@gluestack-ui/form-control']: path.resolve(
__dirname,
'../form-control/src'
),
['@gluestack-ui/utils']: path.resolve(
__dirname,
'../utils/src'
),
// ['@gluestack-ui/utils']: path.resolve(__dirname, '../utils/src'),
// For development, we want to alias the library to the source
},
},
]
: ['babel-plugin-react-docgen-typescript', { exclude: 'node_modules' }],
],
};
};
82 changes: 82 additions & 0 deletions packages/unstyled/pin-input/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
{
"name": "@gluestack-ui/pin-input",
"description": "A universal headless pin-input component for React Native, Next.js & React",
"version": "0.0.2",
"main": "lib/index",
"module": "lib/index",
"types": "lib/index.d.ts",
"react-native": "src/index",
"source": "src/index",
"typings": "lib/index.d.ts",
"scripts": {
"prepare": "tsc",
"release": "release-it",
"watch": "tsc --watch",
"build": "tsc",
"clean": "rm -rf lib",
"dev:web": "cd example/native && yarn web --clear",
"storybook": "cd example/native/storybook && yarn web"
},
"devDependencies": {
"@types/react": "^18.0.22",
"@types/react-native": "^0.72.3",
"babel-plugin-transform-remove-console": "^6.9.4",
"react": "^18.1.0",
"react-dom": "^18.1.0",
"react-native": "^0.72.4",
"react-native-builder-bob": "^0.20.1",
"react-native-web": "^0.19.9",
"tsconfig": "7",
"typescript": "^5.6.3"
},
"dependencies": {
"@gluestack-ui/form-control": "^0.1.19",
"@gluestack-ui/utils": "^0.1.14",
"@react-native-aria/focus": "^0.2.9",
"@react-native-aria/interactions": "0.2.13",
"@react-native-clipboard/clipboard": "^1.15.0"
},
"peerDependencies": {
"react": ">=16",
"react-dom": ">=16"
},
"homepage": "https://github.com/gluestack/gluestack-ui/tree/main/packages/unstyled/pin-input#readme",
"repository": {
"type": "git",
"url": "git+https://github.com/gluestack/gluestack-ui.git"
},
"files": [
"lib/",
"src/"
],
"jest": {
"preset": "jest-expo",
"transform": {
"^.+\\.js$": "<rootDir>/node_modules/react-native/jest/preprocessor.js"
},
"modulePathIgnorePatterns": [
"<rootDir>/example/*",
"<rootDir>/lib/"
],
"transformIgnorePatterns": [
"node_modules/(?!(@react-native|react-native|expo-asset|expo-constants|@unimodules|react-native-unimodules|expo-font|react-native-svg|@expo/vector-icons|react-native-vector-icons|@react-native-aria/checkbox|@react-native-aria/interactions|@react-native-aria/button|@react-native-aria/switch|@react-native-aria/toggle|@react-native-aria/utils|@react-native-aria/*))"
],
"setupFiles": [
"<rootDir>/src/jest/mock.ts"
]
},
"keywords": [
"react",
"native",
"react-native",
"pin-input",
"gluestack-ui",
"universal",
"headless",
"typescript",
"component",
"android",
"ios",
"nextjs"
]
}
158 changes: 158 additions & 0 deletions packages/unstyled/pin-input/src/PinInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import React, { forwardRef, useMemo, useRef, useState } from 'react';
import { Platform } from 'react-native';
import { useHover } from '@react-native-aria/interactions';
import { useFocusRing } from '@react-native-aria/focus';
import { useFormControl } from '@gluestack-ui/form-control';
import { usePinInput } from './PinInputContext';
import { mergeRefs } from '@gluestack-ui/utils';

export const PinInput = (StyledPinInput: any) =>
forwardRef(
(
{
children,
onKeyPress,
type,
'aria-label': ariaLabel = 'Input Field',
secureTextEntry,
editable,
disabled,
index,
...props
}: any,
ref?: any
) => {
const {
isDisabled,
isReadOnly,
isInvalid,
childRefs,
isRequired,
setInputValue,
inputValue,
handleBackSpace,
} = usePinInput('PinInputContext');

// @ts-ignore
const value = inputValue?.length > index ? inputValue[index] : '';

const inputRef = useRef();

const [isFocused, setIsFocused] = useState(false);

const { isHovered } = useHover({}, inputRef);

const inputProps = useFormControl({
isDisabled: props.isDisabled || disabled,
isInvalid: props.isInvalid,
isReadOnly: props.isReadOnly,
isRequired: props.isRequired,
id: props.id,
});

const handlePaste = (pastedValue: any) => {
// const newPastedValue = pastedValue.slice(0, -1);
// // console.log('newPastedValue', newPastedValue);
setInputValue(pastedValue);
};

const { isFocusVisible }: any = useFocusRing();

const mergedRef = mergeRefs([ref, inputRef, childRefs[index]]);

const handleFocus = (focusState: boolean, callback: any) => {
setIsFocused(focusState);
callback();
};

const editableProp = useMemo(() => {
if (editable !== undefined) {
return editable;
} else {
return isDisabled || inputProps.isDisabled || isReadOnly
? false
: true;
}
}, [isDisabled, inputProps.isDisabled, isReadOnly, editable]);

const handleChange = (currentValue: string) => {
if (currentValue.length > 2) {
handlePaste(currentValue);
} else {
setInputValue((prev: string) => {
if (currentValue === '') {
return prev.slice(0, -1);
}
if (currentValue.length === 1) {
prev += currentValue;
return prev;
}
let newPrev = prev.slice(0, -1);
newPrev = newPrev + currentValue.slice(-1);
return newPrev;
});
}
props.onChange && props.onChange(currentValue);
};

return (
<StyledPinInput
keyboardType="numeric"
{...props}
value={value}
states={{
focus: isFocused,
invalid: isInvalid,
readonly: isReadOnly,
required: isRequired,
hover: isHovered,
focusVisible: isFocusVisible,
disabled: isDisabled || inputProps.isDisabled,
}}
dataSet={{
focus: isFocused ? 'true' : 'false',
invalid: isInvalid ? 'true' : 'false',
readonly: isReadOnly ? 'true' : 'false',
required: isRequired ? 'true' : 'false',
hover: isHovered ? 'true' : 'false',
focusVisible: isFocusVisible ? 'true' : 'false',
disabled: isDisabled || inputProps.isDisabled ? 'true' : 'false',
}}
disabled={isDisabled || inputProps.isDisabled}
secureTextEntry={secureTextEntry || type === 'password'}
accessible
aria-label={ariaLabel}
aria-required={isRequired || inputProps.isRequired}
aria-invalid={isInvalid || inputProps.isInvalid}
aria-disabled={isDisabled || inputProps.isDisabled}
aria-selected={Platform.OS !== 'web' ? isFocused : undefined}
// ios accessibility
accessibilityElementsHidden={isDisabled || inputProps.isDisabled}
autoComplete="one-time-code"
textContentType="oneTimeCode"
readOnly={!editableProp}
onKeyPress={(e: any) => {
e.nativeEvent.key !== 'Backspace' && e.persist();
onKeyPress && onKeyPress(e);
e.nativeEvent.key === 'Backspace' && handleBackSpace(index, value);
}}
onFocus={(e: any) => {
handleFocus(
true,
props?.onFocus ? () => props?.onFocus(e) : () => {}
);
}}
onBlur={(e: any) => {
handleFocus(
false,
props?.onBlur ? () => props?.onBlur(e) : () => {}
);
}}
onChangeText={handleChange}
ref={mergedRef}
>
{children}
</StyledPinInput>
);
}
);
5 changes: 5 additions & 0 deletions packages/unstyled/pin-input/src/PinInputContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { IPinInputContext } from './types';
import { createContext } from '@gluestack-ui/utils';

export const [PinInputProvider, usePinInput] =
createContext<IPinInputContext>('PinInputContext');
Loading

0 comments on commit 984ea17

Please sign in to comment.