diff --git a/README.md b/README.md index a75efc8..55e4e7a 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,6 @@ Some of the unique features this component offers include: - Minimalistic, visually pleasing style - Variable content length - ## Used by - [caseconverter.pro](https://caseconverter.pro/app) @@ -35,38 +34,40 @@ Some of the unique features this component offers include:
Install via NPM - ```bash - npm install --save react-in-out-textarea - # You might want to install react-tooltip if you activate the max length option - npm install --save react-tooltip - ``` +```bash +npm install --save react-in-out-textarea +# You might want to install react-tooltip if you activate the max length option +npm install --save react-tooltip +``` +
Install via yarn - ```bash - yarn add react-in-out-textarea - # You might want to install react-tooltip if you activate the max length option - yarn add react-tooltip - ``` +```bash +yarn add react-in-out-textarea +# You might want to install react-tooltip if you activate the max length option +yarn add react-tooltip +``` +
## Props -| Name | Type | Required | Description | -| --- | --- | --- | --- | -| inValue | string | ✔️ | The value that is shown on the left-handed side. | -| outValue | string | ✔️ | The value that is shown on the right-handed side. | -| inOptions | array | ✔️ | An array of options filled with names marked true or false | -| onInInput | function | ✔️ | Called to listen to when the text on the left-hand side changes | ✔️ | -| onInOptionsUpdate | function | ✔️ | Updated with new options as the parameter when inOptions language clicked | -| outOptions | array | ✔️ | An array of options filled with names marked true or false and an activeClicked boolean | -| onOutOptionsUpdate | function | ✔️ | Updated with new options as the parameter when outOptions language clicked | -| maxContentLength | number | ❌ | Value that defines the maximum number of characters allowed in the text area. | -| maxContentLengthIndicator | Object | ❌ | An Object describing how the length indicator is shown. | -| onCopy | function | ❌ | A function that is called when you have copied the content of `InOutTextarea`. | -| autoCloseMenuOnOptionSelection | boolean | ❌ | Boolean that defines whether an option menu should self-close after selection. | +| Name | Type | Required | Description | +| ------------------------------ | -------- | -------- | ------------------------------------------------------------------------------ | +| inValue | string | ✔️ | The value that is shown on the left-handed side. | +| outValue | string | ✔️ | The value that is shown on the right-handed side. | +| inOptions | array | ✔️ | An array of options filled with names marked true or false | +| onInInput | function | ✔️ | Called to listen to when the text on the left-hand side changes | +| onInOptionsUpdate | function | ✔️ | Updated with new options as the parameter when inOptions language clicked | +| outOptions | array | ✔️ | An array of options filled with names marked true or false | +| onOutOptionsUpdate | function | ✔️ | Updated with new options as the parameter when outOptions language clicked | +| maxContentLength | number | ❌ | Value that defines the maximum number of characters allowed in the text area. | +| maxContentLengthIndicator | Object | ❌ | An Object describing how the length indicator is shown. | +| onCopy | function | ❌ | A function that is called when you have copied the content of `InOutTextarea`. | +| autoCloseMenuOnOptionSelection | boolean | ❌ | Boolean that defines whether an option menu should self-close after selection. | ## Usage @@ -84,7 +85,7 @@ import { InOutTextarea, InOptions, OutOptions } from 'react-in-out-textarea'; export const ExampleComponent = () => { const [inValue, setInValue] = useState(''); const [outValue, setOutValue] = useState(''); - const [inOptions, setInOptions] = useState([ + const [inOptions, setInOptions] = useState([ { name: 'English', active: true, @@ -94,11 +95,10 @@ export const ExampleComponent = () => { active: false, }, ]); - const [outOptions, setOutOptions] = useState([ + const [outOptions, setOutOptions] = useState([ { name: 'Chinese', active: true, - activeClicked: false, }, ]); @@ -106,16 +106,16 @@ export const ExampleComponent = () => { { + onInInput={newValue => { setInValue(newValue); setOutValue(newValue); }} inOptions={inOptions} - onInOptionsUpdate={(newInOptions) => { + onInOptionsUpdate={newInOptions => { setInOptions(newInOptions); }} outOptions={outOptions} - onOutOptionsUpdate={(newOutOptions) => { + onOutOptionsUpdate={newOutOptions => { setOutOptions(newOutOptions); }} /> @@ -128,57 +128,56 @@ export const ExampleComponent = () => {
React + Javascript - [CodeSandbox Example](https://codesandbox.io/s/react-in-out-textarea-javascript-react-kcl37?file=/src/ExampleComponent.js) - - Code Example: - - ```js - import React, { useState } from "react"; - import { InOutTextarea } from "react-in-out-textarea"; - - export const ExampleComponent = () => { - const [inValue, setInValue] = useState(""); - const [outValue, setOutValue] = useState(""); - const [inOptions, setInOptions] = useState([ - { - name: "English", - active: true - }, - { - name: "German", - active: false - } - ]); - const [outOptions, setOutOptions] = useState([ - { - name: "Chinese", - active: true, - activeClicked: false - } - ]); - - return ( - { - setInValue(newValue); - setOutValue(newValue); - }} - inOptions={inOptions} - onInOptionsUpdate={(newInOptions) => { - setInOptions(newInOptions); - }} - outOptions={outOptions} - onOutOptionsUpdate={(newOutOptions) => { - setOutOptions(newOutOptions); - }} - /> - ); - }; - ``` -
+[CodeSandbox Example](https://codesandbox.io/s/react-in-out-textarea-javascript-react-kcl37?file=/src/ExampleComponent.js) + +Code Example: + +```js +import React, { useState } from 'react'; +import { InOutTextarea } from 'react-in-out-textarea'; +export const ExampleComponent = () => { + const [inValue, setInValue] = useState(''); + const [outValue, setOutValue] = useState(''); + const [inOptions, setInOptions] = useState([ + { + name: 'English', + active: true, + }, + { + name: 'German', + active: false, + }, + ]); + const [outOptions, setOutOptions] = useState([ + { + name: 'Chinese', + active: true, + }, + ]); + + return ( + { + setInValue(newValue); + setOutValue(newValue); + }} + inOptions={inOptions} + onInOptionsUpdate={newInOptions => { + setInOptions(newInOptions); + }} + outOptions={outOptions} + onOutOptionsUpdate={newOutOptions => { + setOutOptions(newOutOptions); + }} + /> + ); +}; +``` + + ## Development diff --git a/src/CaseButton.tsx b/src/CaseButton.tsx index 6be8048..74d7167 100644 --- a/src/CaseButton.tsx +++ b/src/CaseButton.tsx @@ -2,7 +2,6 @@ import styled from 'styled-components'; type CaseButtonProps = { active?: boolean; - activeClicked?: boolean; }; export const CaseButton = styled.div` @@ -14,13 +13,13 @@ export const CaseButton = styled.div` cursor: pointer; color: ${props => { if (props.theme.main === 'dark') { - if (props.active || props.activeClicked) { + if (props.active) { return '#fff'; } else { return '#E5E5E5'; } } else { - if (props.active || props.activeClicked) { + if (props.active) { return '#14213d'; } else { return 'color: rgba(20,33,61,0.4);'; @@ -29,9 +28,7 @@ export const CaseButton = styled.div` }}; border-bottom: ${props => { if (props.active) { - return '2px solid #fca311'; - } else if (props.activeClicked) { - return '2px solid #5ba4ca'; + return '2px solid var(--border-active-color)'; } else { return '2px solid transparent'; } diff --git a/src/InMenuOptionStuff.tsx b/src/InMenuOptionStuff.tsx index 2622d28..0e6e0e0 100644 --- a/src/InMenuOptionStuff.tsx +++ b/src/InMenuOptionStuff.tsx @@ -1,61 +1,6 @@ -import React from 'react'; -import useDimensions, { IDimensionValues } from 'react-use-dimensions'; -import { CaseButton } from './CaseButton'; -import { IInOption, InOptions } from './types'; +import styled from 'styled-components'; +import { MenuOptionStuff } from './MenuOptionStuff'; -interface IInMenuOptionStuff { - inOptionsMenuRefSizes: IDimensionValues; - liveMeasure: boolean; - menuOptions: InOptions; - option: IInOption; - inOptions: InOptions; - onInOptionsUpdate: (newInOptions: InOptions) => void; - setMenuOptions: React.Dispatch>; -} - -export const InMenuOptionStuff = (props: IInMenuOptionStuff) => { - const { - inOptionsMenuRefSizes, - liveMeasure, - menuOptions, - option, - inOptions, - onInOptionsUpdate, - setMenuOptions, - } = props; - - const [suuuRef, suuSizes] = useDimensions({ liveMeasure }); - if (!inOptionsMenuRefSizes) return null; - - const shouldHide = suuSizes.x + suuSizes.width > inOptionsMenuRefSizes.x; - - if (shouldHide) { - if (menuOptions.find((e: IInOption) => e.name === option.name)) { - return null; - } - setMenuOptions([...menuOptions, option]); - return null; - } else { - if (menuOptions.find((e: IInOption) => e.name === option.name)) { - setMenuOptions([...menuOptions.filter(e => e.name !== option.name)]); - } - } - - return ( - { - const updatedOptions: InOptions = [ - ...inOptions.map((inOption: IInOption) => ({ - ...inOption, - active: inOption.name === option.name, - })), - ]; - onInOptionsUpdate(updatedOptions); - }} - > - {option.name} - - ); -}; +export const InMenuOptionStuff = styled(MenuOptionStuff)` + --border-active-color: #fca311; +`; diff --git a/src/MenuOptionStuff.tsx b/src/MenuOptionStuff.tsx new file mode 100644 index 0000000..2972d9e --- /dev/null +++ b/src/MenuOptionStuff.tsx @@ -0,0 +1,64 @@ +import React from 'react'; +import useDimensions, { IDimensionValues } from 'react-use-dimensions'; +import { CaseButton } from './CaseButton'; +import { IOption, Options } from './types'; + +export interface IMenuOptionStuff { + optionsMenuRefSizes: IDimensionValues; + liveMeasure: boolean; + menuOptions: Options; + option: IOption; + options: Options; + onOptionsUpdate: (newOptions: Options) => void; + setMenuOptions: React.Dispatch>; + className?: string; +} + +export const MenuOptionStuff = (props: IMenuOptionStuff) => { + const { + optionsMenuRefSizes, + liveMeasure, + menuOptions, + option, + options, + onOptionsUpdate, + setMenuOptions, + className, + } = props; + + const [suuuRef, suuSizes] = useDimensions({ liveMeasure }); + if (!optionsMenuRefSizes) return null; + + const shouldHide = suuSizes.x + suuSizes.width > optionsMenuRefSizes.x; + + if (shouldHide) { + if (menuOptions.find((e: IOption) => e.name === option.name)) { + return null; + } + setMenuOptions([...menuOptions, option]); + return null; + } else { + if (menuOptions.find((e: IOption) => e.name === option.name)) { + setMenuOptions([...menuOptions.filter(e => e.name !== option.name)]); + } + } + + return ( + { + const updatedOptions: Options = [ + ...options.map((inOption: IOption) => ({ + ...inOption, + active: inOption.name === option.name, + })), + ]; + onOptionsUpdate(updatedOptions); + }} + > + {option.name} + + ); +}; diff --git a/src/OptionsOverlay.tsx b/src/OptionsOverlay.tsx index ff33f9d..c0fb2f7 100644 --- a/src/OptionsOverlay.tsx +++ b/src/OptionsOverlay.tsx @@ -1,7 +1,7 @@ import React from 'react'; import styled from 'styled-components'; import { IDimensionValues } from 'react-use-dimensions'; -import { IInOption, IOutOption } from './types'; +import { IOption } from './types'; interface IContainer { minHeight?: string; @@ -48,14 +48,14 @@ const OverlayOption = styled.div` } `; -type Options = Array; +type Options = Array; interface IOptionsOverlay { convertCardSizes: IDimensionValues; shownMenuOptions: T; allMenuOptions: T; onAllMenuOptionsUpdate: (updatedOptions: T) => void; - onOptionClick: (option: IInOption | IOutOption) => void; + onOptionClick: (option: IOption) => void; } export const OptionsOverlay = ( @@ -74,10 +74,10 @@ export const OptionsOverlay = ( minHeight={`${convertCardSizes.height}px`} maxHeight={`${convertCardSizes.height}px`} > - {shownMenuOptions.map((option) => { + {shownMenuOptions.map(option => { return ( { const updatedOptions = [ ...allMenuOptions.map(outOption => ({ diff --git a/src/OutMenuOptionStuff.tsx b/src/OutMenuOptionStuff.tsx index 6beeb93..af1758f 100644 --- a/src/OutMenuOptionStuff.tsx +++ b/src/OutMenuOptionStuff.tsx @@ -1,61 +1,6 @@ -import React from 'react'; -import useDimensions, { IDimensionValues } from 'react-use-dimensions'; -import { CaseButton } from './CaseButton'; -import { IOutOption, OutOptions } from './types'; +import styled from 'styled-components'; +import { MenuOptionStuff } from './MenuOptionStuff'; -interface IOutMenuOptionStuff { - outOptionsMenuRefSizes: IDimensionValues; - liveMeasure: boolean; - menuOptions: OutOptions; - option: IOutOption; - outOptions: OutOptions; - onOutOptionsUpdate: (newInOptions: OutOptions) => void; - setMenuOptions: React.Dispatch>; -} - -export const OutMenuOptionStuff = (props: IOutMenuOptionStuff) => { - const { - outOptionsMenuRefSizes: inOptionsMenuRefSizes, - liveMeasure, - menuOptions, - option, - outOptions: inOptions, - onOutOptionsUpdate: onInOptionsUpdate, - setMenuOptions, - } = props; - - const [suuuRef, suuSizes] = useDimensions({ liveMeasure }); - if (!inOptionsMenuRefSizes) return null; - - const shouldHide = suuSizes.x + suuSizes.width > inOptionsMenuRefSizes.x; - - if (shouldHide) { - if (menuOptions.find((e: IOutOption) => e.name === option.name)) { - return null; - } - setMenuOptions([...menuOptions, option]); - return null; - } else { - if (menuOptions.find((e: IOutOption) => e.name === option.name)) { - setMenuOptions([...menuOptions.filter(e => e.name !== option.name)]); - } - } - - return ( - { - const updatedOptions: OutOptions = [ - ...inOptions.map((inOption: IOutOption) => ({ - ...inOption, - active: inOption.name === option.name, - })), - ]; - onInOptionsUpdate(updatedOptions); - }} - > - {option.name} - - ); -}; +export const OutMenuOptionStuff = styled(MenuOptionStuff)` + --border-active-color: #5ba4ca; +`; diff --git a/src/index.tsx b/src/index.tsx index 83d88b3..4c390e9 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -7,7 +7,7 @@ import { IconCopy } from './IconCopy'; import { CopyToClipboard } from 'react-copy-to-clipboard'; import { InMenuOptionStuff } from './InMenuOptionStuff'; import { OutMenuOptionStuff } from './OutMenuOptionStuff'; -import { IInOption, InOptions, IOutOption, OutOptions } from './types'; +import { IOption, Options } from './types'; import { CaseBar } from './CaseBar'; import { SideBar } from './SideBar'; import { TextAreaContentTop, TextAreaContentBottom } from './TextAreaContent'; @@ -22,7 +22,7 @@ import { } from './MaxContentLengthIndicator'; import { Textarea } from './styled/Textarea'; -export { IInOption, IOutOption, InOptions, OutOptions }; +export { IOption, Options }; const ConvertCardContent = styled.div` width: 100%; @@ -49,13 +49,13 @@ const Flex = styled.div` const liveMeasure = true; export interface Props extends HTMLAttributes { - inOptions: InOptions; + inOptions: Options; inValue: string; outValue: string; onInInput: (text: string) => void; - onInOptionsUpdate: (newInOptions: InOptions) => void; - outOptions: OutOptions; - onOutOptionsUpdate: (newOutOptions: OutOptions) => void; + onInOptionsUpdate: (newInOptions: Options) => void; + outOptions: Options; + onOutOptionsUpdate: (newOutOptions: Options) => void; maxContentLength?: number; onCopy?: () => void; maxContentLengthIndicator?: null | IMaxContentLengthIndicator; @@ -63,8 +63,8 @@ export interface Props extends HTMLAttributes { } export const InOutTextarea: FC = props => { - const [menuInOptions, setMenuInOptions] = useState([]); - const [menuOutOptions, setMenuOutOptions] = useState([]); + const [menuInOptions, setMenuInOptions] = useState([]); + const [menuOutOptions, setMenuOutOptions] = useState([]); const [inOptionsMenuRef, inOptionsMenuRefSizes] = useDimensions({ liveMeasure, }); @@ -123,16 +123,16 @@ export const InOutTextarea: FC = props => { if (a.active) return -1; return 0; }) - .map((option) => { + .map(option => { return ( ); @@ -149,20 +149,19 @@ export const InOutTextarea: FC = props => { {outOptions .sort(a => { - if (a.activeClicked) return -1; if (a.active) return -1; return 0; }) - .map((option) => { + .map(option => { return ( ); diff --git a/src/types.ts b/src/types.ts index 86708e8..9983226 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,13 +1,20 @@ -export interface IInOption { +/** + * An option that can be shown on the selector + */ +export interface IOption { + /** + * The name/label to show for the option. + * + * For example, 'English' or 'German' in the Google Translate example + */ name: string; + /** Is this option active */ active: boolean; + /** Optional key for React iteration - will use `name` by default */ key?: string; } -export type InOptions = Array; - -export interface IOutOption extends IInOption { - activeClicked: boolean; -} - -export type OutOptions = Array; +/** + * A list of options to be shown + */ +export type Options = Array; diff --git a/stories/Thing.stories.tsx b/stories/Thing.stories.tsx index aa76d84..52c9ebe 100644 --- a/stories/Thing.stories.tsx +++ b/stories/Thing.stories.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import styled, { ThemeProvider } from 'styled-components'; import ReactTooltip from 'react-tooltip'; -import { InOutTextarea, Props, InOptions, OutOptions } from '../src'; +import { InOutTextarea, Props, Options } from '../src'; export default { title: 'Welcome', @@ -29,7 +29,7 @@ export default { // you consume the story in a test. export const Default = (props?: Partial) => { const [inValue, setInValue] = useState('Hello'); - const [inOptions, setInOptions] = useState([ + const [inOptions, setInOptions] = useState([ { name: 'English', active: false, @@ -88,51 +88,42 @@ export const Default = (props?: Partial) => { }, ]); - const [outOptions, setOutOptions] = useState([ + const [outOptions, setOutOptions] = useState([ { name: 'English', active: true, - activeClicked: false, }, { name: 'German', active: false, - activeClicked: false, }, { name: 'Russian', active: false, - activeClicked: false, }, { name: 'Chinese 1', active: false, - activeClicked: false, }, { name: 'Chinese 2', active: false, - activeClicked: false, }, { name: 'Chinese 3', active: false, - activeClicked: false, }, { name: 'Chinese 4', active: false, - activeClicked: false, }, { name: 'Chinese 5', active: false, - activeClicked: false, }, { name: 'Chinese 6', active: false, - activeClicked: false, }, ]); @@ -193,7 +184,7 @@ const _WithLengthLimit = ({ .reverse() .join('')} inOptions={[{ active: true, name: 'English' }]} - outOptions={[{ active: true, name: 'German', activeClicked: true }]} + outOptions={[{ active: true, name: 'German' }]} onInInput={setInValue} onInOptionsUpdate={() => true} onOutOptionsUpdate={() => true} @@ -216,3 +207,61 @@ export const WithLengthLimit = _WithLengthLimit.bind({}); WithLengthLimit.args = { maxContentLength: 100, }; + +export const WithCustomKey = (props?: Partial) => { + const [inValue, setInValue] = useState('Hello'); + const [inOptions, setInOptions] = useState([ + { + name: 'English', + key: 'en', + active: false, + }, + { + name: 'German', + key: 'de', + active: true, + }, + { + name: 'Russian', + key: 'ru', + active: false, + }, + ]); + + const [outOptions, setOutOptions] = useState([ + { + name: 'English', + key: 'en', + active: true, + }, + { + name: 'German', + key: 'de', + active: false, + }, + { + name: 'Russian', + key: 'ru', + active: false, + }, + ]); + + return ( +
+ setInValue(newValue)} + inOptions={inOptions} + onInOptionsUpdate={newInOptions => { + setInOptions(newInOptions); + }} + outOptions={outOptions} + onOutOptionsUpdate={newOutOptions => { + setOutOptions(newOutOptions); + }} + outValue={'Hello'} + /> +
+ ); +};