import React, { useState } from 'react' import { Passphrase } from 'playbook-ui' const PassphraseDefault = (props) => { const [input, setInput] = useState('') const handleChange = (e) => setInput(e.target.value) return ( <Passphrase id="my-passphrase" onChange={handleChange} value={input} /> ) } export default PassphraseDefault
import React, { useState } from 'react' import { Body, Passphrase } from 'playbook-ui' const PassphraseConfirmation = (props) => { const [input, setInput] = useState('') const [confirmationInput, setConfirmationInput] = useState('') const handleChange = (e) => setInput(e.target.value) const handleConfirmationChange = (e) => setConfirmationInput(e.target.value) return ( <> <div> <Passphrase onChange={handleChange} value={input} /> <Passphrase confirmation onChange={handleConfirmationChange} value={confirmationInput} /> {input && confirmationInput && ( <Body text={ input === confirmationInput ? "They match!" : "They don't match!" } /> )} </div> </> ); } export default PassphraseConfirmation
This example shows how to enhance the passphrase strenght by setting diferent thresholds and lengths.
The meterSettings
array contains different settings for each rendered input. The handleStrengthCalculation
handles the strength calculation using those settings, showing different results for the same passphrase
input.
By default, minLength
is 12. Try typing any value in the Default Example
input. Notice that the bar won't change from red until the minimum is met.
Adjust these props to tune the sensitivity of the bar.
Note: minimum length trumps strength and will set the bar to a red color, despite whatever strength is calculated.
This example depends on the zxcvbn
library.
You can use any library to achieve the same result, this example only intends to show how to add more features to the Passphrase
kit.
import React, { useState } from 'react' import { Body, Passphrase, ProgressSimple, Caption, TextInput } from 'playbook-ui' import zxcvbn from 'zxcvbn' const PassphraseMeterSettings = (props) => { const [input, setInput] = useState('') const [result, setResult] = useState({}) const [calculatedStrength, setCalculatedStrength] = useState(0) const meterSettings = [ { label: "Default settings" }, { minLength: 5, label: "Min length = 5", }, { minLength: 30, label: "Min length = 30", }, { label: "Average threshold = 1", averageThreshold: 1, }, { label: "Strong Threshold = 4", strongThreshold: 4, }, ] const handleStrengthCalculation = (settings) => { const { passphrase = "", common = false, isPwned = false, averageThreshold = 2, minLength = 12, strongThreshold = 3, } = settings const resultByScore = { 0: { variant: 'negative', label: '', percent: 0, }, 1: { variant: 'negative', label: 'This passphrase is too common', percent: 25, }, 2: { variant: 'negative', label: 'Too weak', percent: 25, }, 3: { variant: 'warning', label: 'Almost there, keep going!', percent: 50, }, 4: { variant: 'positive', label: 'Success! Strong passphrase', percent: 100, } } const { score } = zxcvbn(passphrase); const noPassphrase = passphrase.length <= 0 const commonPassphrase = common || isPwned const weakPassphrase = passphrase.length < minLength || score < averageThreshold const averagePassphrase = score < strongThreshold const strongPassphrase = score >= strongThreshold if (noPassphrase) { return {...resultByScore[0], score} } else if (commonPassphrase) { return {...resultByScore[1], score} } else if (weakPassphrase) { return {...resultByScore[2], score} } else if (averagePassphrase){ return {...resultByScore[3], score} } else if (strongPassphrase) { return {...resultByScore[4], score} } } const handleChange = (e) => { const passphrase = e.target.value; setInput(passphrase) const calculated = [] meterSettings.forEach((setting, index) => { const results = handleStrengthCalculation({passphrase, ...setting}) if (index == 0) setCalculatedStrength(results.score) calculated.push(results) }) setResult(calculated) } return ( <> <div> <Body> { "These examples will all share the same input value. Type in any of the inputs to see how the strength meter changes in response to different settings." } </Body> <Passphrase label={"Type your passphrase"} onChange={handleChange} value={input} /> <TextInput disabled label="Calculated Strength" readOnly value={calculatedStrength} /> {meterSettings.map((settings, index) => ( <div key={index}> <Passphrase label={settings.label} onChange={handleChange} value={input} /> {input.length > 0 && ( <> <ProgressSimple percent={result[index].percent} variant={result[index].variant} /> <Caption size='xs' text={result[index].label} /> </> )} </div> ))} </div> </> ); } export default PassphraseMeterSettings
inputProps
is passed directly to an underlying Text Input kit. See the specific docs here for more details.
import React, { useState } from 'react' import { Passphrase } from 'playbook-ui' const PassphraseInputProps = (props) => { const [input, setInput] = useState('') const handleChange = (e) => setInput(e.target.value) return ( <> <div> <Passphrase inputProps={{ name: 'my-disabled-field', id: 'my-value-id', disabled: true, }} label="Pass props directly to input kit" onChange={handleChange} value={input} /> <Passphrase inputProps={{ children: ( <input onChange={handleChange} type="password" value={input} />), }} label="Custom input" onChange={handleChange} value={input} /> <Passphrase inputProps={{ name: 'my-value-name', id: 'my-value-id-2' }} label="Set name and ID for use in form libraries" onChange={handleChange} value={input} /> <Passphrase confirmation inputProps={{ name: 'my-value-confirmation-name', id: 'my-value-confirmation-id' }} onChange={handleChange} value={input} /> </div> </> ) } export default PassphraseInputProps
showTipsBelow
(react) / show_tips_below
(rails) takes 'xs', 'sm', 'md', 'lg', 'xl' and only show the tips below the given screen size. Similar to the responsive table breakpoints. Omit this prop to always show.
import React, { useState } from 'react' import { Passphrase } from 'playbook-ui' const PassphraseTips = (props) => { const [input, setInput] = useState('') const handleChange = (e) => setInput(e.target.value) return ( <> <div> <Passphrase label="Pass an array of strings to the tips prop" onChange={handleChange} tips={['And the info icon will appear.', 'Each string will be displayed as its own tip']} value={input} /> <Passphrase label="Omit the prop to hide the icon" onChange={handleChange} value={input} /> <Passphrase label="Only show tips at small screen size" onChange={handleChange} showTipsBelow="xs" tips={['Make the password longer', 'Type more things', 'Use something else']} value={input} /> <Passphrase label="Only show tips at medium screen size" onChange={handleChange} showTipsBelow="md" tips={['Make the password longer', 'Type more things', 'Use something else']} value={input} /> <Passphrase label="Only show tips at large screen size" onChange={handleChange} showTipsBelow="lg" tips={['Make the password longer', 'Type more things', 'Use something else']} value={input} /> </div> </> ) } export default PassphraseTips
Strength is calculated on a 0-4 scale by the Zxcvbn package.
This example depends on the zxcvbn
library.
You can use any library to achieve the same result, this example only intends to show how to add more features to the Passphrase
kit.
import React, { useState } from 'react' import { Passphrase, Caption, ProgressSimple, TextInput } from 'playbook-ui'import { useEffect } from 'react' import zxcvbn from 'zxcvbn' const PassphraseStrengthChange = (props) => { const [input, setInput] = useState('') const [checkStrength, setCheckStrength] = useState({ label: '', percent: 0, score: 0, variant: '' }) const handleChange = (e) => setInput(e.target.value) const handleStrengthCalculation = (settings) => { const { passphrase = "", common = false, isPwned = false, averageThreshold = 2, minLength = 12, strongThreshold = 3, } = settings const resultByScore = { 0: { variant: 'negative', label: '', percent: 0, }, 1: { variant: 'negative', label: 'This passphrase is too common', percent: 25, }, 2: { variant: 'negative', label: 'Too weak', percent: 25, }, 3: { variant: 'warning', label: 'Almost there, keep going!', percent: 50, }, 4: { variant: 'positive', label: 'Success! Strong passphrase', percent: 100, } } const { score } = zxcvbn(passphrase); const noPassphrase = passphrase.length <= 0 const commonPassphrase = common || isPwned const weakPassphrase = passphrase.length < minLength || score < averageThreshold const averagePassphrase = score < strongThreshold const strongPassphrase = score >= strongThreshold if (noPassphrase) { return {...resultByScore[0], score} } else if (commonPassphrase) { return {...resultByScore[1], score} } else if (weakPassphrase) { return {...resultByScore[2], score} } else if (averagePassphrase){ return {...resultByScore[3], score} } else if (strongPassphrase) { return {...resultByScore[4], score} } } useEffect(() => { const result = handleStrengthCalculation({ passphrase: input }) setCheckStrength({...result}) },[input]) return ( <> <Passphrase label="Passphrase" onChange={handleChange} value={input} /> {input.length > 0 && ( <> <ProgressSimple percent={checkStrength.percent} variant={checkStrength.variant} /> <Caption size='xs' text={checkStrength.label} /> </> )} <TextInput disabled label="Passphrase Strength" marginTop="xl" readOnly value={checkStrength.score} /> </> ) } export default PassphraseStrengthChange
This example depends on the zxcvbn
library.
You can use any library to achieve the same result, this example only intends to show how to add more features to the Passphrase
kit.
import React, { useState, useEffect } from 'react' import { Body, Passphrase, Caption, ProgressSimple } from 'playbook-ui' import zxcvbn from 'zxcvbn' const PassphraseCommon = (props) => { const [input, setInput] = useState('') const [checkStrength, setCheckStrength] = useState({ label: '', percent: 0, score: 0, variant: '' }) const handleChange = (e) => setInput(e.target.value) const handleStrengthCalculation = (settings) => { const { passphrase = "", common = false, isPwned = false, averageThreshold = 2, minLength = 12, strongThreshold = 3, } = settings const resultByScore = { 0: { variant: 'negative', label: '', percent: 0, }, 1: { variant: 'negative', label: 'This passphrase is too common', percent: 25, }, 2: { variant: 'negative', label: 'Too weak', percent: 25, }, 3: { variant: 'warning', label: 'Almost there, keep going!', percent: 50, }, 4: { variant: 'positive', label: 'Success! Strong passphrase', percent: 100, } } const { score } = zxcvbn(passphrase); const noPassphrase = passphrase.length <= 0 const commonPassphrase = common || isPwned const weakPassphrase = passphrase.length < minLength || score < averageThreshold const averagePassphrase = score < strongThreshold const strongPassphrase = score >= strongThreshold if (noPassphrase) { return {...resultByScore[0], score} } else if (commonPassphrase) { return {...resultByScore[1], score} } else if (weakPassphrase) { return {...resultByScore[2], score} } else if (averagePassphrase){ return {...resultByScore[3], score} } else if (strongPassphrase) { return {...resultByScore[4], score} } } const COMMON_PASSPHRASES = ['passphrase', 'apple', 'password', 'p@55w0rd'] const isCommon = (passphrase) => { if (COMMON_PASSPHRASES.includes(passphrase)) return true return false } useEffect(() => { const result = handleStrengthCalculation({ passphrase: input, common: isCommon(input) }); setCheckStrength({ ...result }) }, [input]) return ( <> <div> <Body marginBottom='md' text={`Try typing any of the following: ${COMMON_PASSPHRASES.join(', ')}`} /> <Passphrase onChange={handleChange} value={input} /> {input.length > 0 && ( <> <ProgressSimple className={input.length === 0 ? "progress-empty-input" : null} percent={checkStrength.percent} variant={checkStrength.variant} /> <Caption size='xs' text={checkStrength.label} /> </> )} </div> </> ) } export default PassphraseCommon
Use HaveIBeenPwned's API to check for breached passwords.
As the passphrase is typed, it is checked against more than half a billion breached passwords, to help ensure its not compromised.
Should it fail, the feedback will express the passphrase is too common, prompting the user to change.
This uses their k-Anonymity model, so only the first 5 characters of a hashed copy of the passphrase are sent.
This example depends on the zxcvbn
library and haveibeenpwned
API.
You can use any library to achieve the same result, this example only intends to show how to add more features to the Passphrase
kit.
import React, { useState, useEffect } from 'react' import { Passphrase, Caption, ProgressSimple } from 'playbook-ui' import zxcvbn from 'zxcvbn' const PassphraseBreached = (props) => { const [input, setInput] = useState('') const [isPwned, setIsPwned] = useState(false) const [checkStrength, setCheckStrength] = useState({ label: '', percent: 0, score: 0, variant: '' }) const handleChange = (e) => setInput(e.target.value) const checkHaveIBeenPwned = async function (passphrase) { const buffer = new TextEncoder('utf-8').encode(passphrase) const digest = await crypto.subtle.digest('SHA-1', buffer) const hashArray = Array.from(new Uint8Array(digest)) const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('') const firstFive = hashHex.slice(0, 5) const endOfHash = hashHex.slice(5) const resp = await fetch(`https://api.pwnedpasswords.com/range/${firstFive}`) const text = await resp.text() const match = text.split('\n').some((line) => { //Each line is <sha-1-hash-suffix>:<count of incidents> return line.split(':')[0] === endOfHash.toUpperCase() }) return match } const handleStrengthCalculation = (settings) => { const { passphrase = "", common = false, isPwned = false, averageThreshold = 2, minLength = 12, strongThreshold = 3, } = settings const resultByScore = { 0: { variant: 'negative', label: '', percent: 0, }, 1: { variant: 'negative', label: 'This passphrase is too common', percent: 25, }, 2: { variant: 'negative', label: 'Too weak', percent: 25, }, 3: { variant: 'warning', label: 'Almost there, keep going!', percent: 50, }, 4: { variant: 'positive', label: 'Success! Strong passphrase', percent: 100, } } const { score } = zxcvbn(passphrase); const noPassphrase = passphrase.length <= 0 const commonPassphrase = common || isPwned const weakPassphrase = passphrase.length < minLength || score < averageThreshold const averagePassphrase = score < strongThreshold const strongPassphrase = score >= strongThreshold if (noPassphrase) { return {...resultByScore[0], score} } else if (commonPassphrase) { return {...resultByScore[1], score} } else if (weakPassphrase) { return {...resultByScore[2], score} } else if (averagePassphrase){ return {...resultByScore[3], score} } else if (strongPassphrase) { return {...resultByScore[4], score} } } useEffect( () => { const delay = 400 const result = handleStrengthCalculation({ passphrase: input, isPwned: isPwned }); setCheckStrength({ ...result }) // only check the API for passphrases above the minimum size if (input.length < 5) { setIsPwned(false) return } const handler = setTimeout(() => { checkHaveIBeenPwned(input) .then((pwned) => setIsPwned(pwned)) .catch(() => setIsPwned(false)) }, delay) return () => { clearTimeout(handler) } }, [input, isPwned] ) return ( <> <div> <br /> <Passphrase onChange={handleChange} value={input} /> {checkStrength.percent > 0 ? <> <ProgressSimple className={input.length === 0 ? "progress-empty-input" : null} percent={checkStrength.percent} variant={checkStrength.variant} /> <Caption size='xs' text={checkStrength.label} /> </> : null} </div> </> ) } export default PassphraseBreached
NOTE: Error state is handled by default, validating length (too long or too short) relative to the selected country’s phone format and enforcing numeric-only values for all countries.
Preferred countries will display in the order they are listed with the first country preselected, and all non-preferred countries listed alphabetically below in the remaining dropdown.
Setting an initial country preselects that country within the input as well as the country dropdown.
Limiting countries removes all countries from the dropdown except your selections.
Excluding countries removes the selected countries from the dropdown.
import React, { useEffect, useState } from "react"; import { Button, FixedConfirmationToast, PhoneNumberInput, Icon } from 'playbook-ui' const PhoneNumberInputValidation = (props) => { const [formErrors, setFormErrors] = useState(""); const [showFormErrors, setShowFormErrors] = useState(false); const [phoneNumber, setPhoneNumber] = useState(""); const [countryCode, setCountryCode] = useState("af"); const handleOnValidate = (valid) => { setFormErrors( valid ? "" : "Please correct the fields below and try again." ); }; const handleOnChange = ({ iso2, number }) => { setCountryCode(iso2); setPhoneNumber(number); }; const handleOnSubmit = (e) => { if (showFormErrors) e.preventDefault() } useEffect(() => { setShowFormErrors(formErrors.length > 0); }, [formErrors]); const error = ( <> <Icon icon="warning" /> Missing phone number. </> ) return ( <form action="" method="get" onSubmit={handleOnSubmit} > {showFormErrors && ( <FixedConfirmationToast marginBottom="md" status="error" text={formErrors} /> )} <PhoneNumberInput error={error} id="validation" initialCountry={countryCode} onChange={handleOnChange} onValidate={handleOnValidate} required value={phoneNumber} /> <Button htmlType="submit" text="Save Phone Number" /> </form> ); }; export default PhoneNumberInputValidation;
To clear a number inside the input element, create a ref
inside your parent component, pass it to the kit's ref
prop, and use ref.current.clearField()
.
clearField()
is a custom function inside the kit to clear numbers and the error message while still providing validation.
import React, { useRef } from 'react' import { Button, PhoneNumberInput } from 'playbook-ui' const PhoneNumberInputClearField = (props) => { // 1. Create a ref - this accesses the kit's input element. const ref = useRef() // 2. Use clearField() to clear the field. const handleClick = () => { ref.current.clearField() } // 3. Pass the ref to the ref prop. return ( <> <PhoneNumberInput id="clear-field" ref={ref} /> <Button onClick={handleClick} text="Clear the Input Field" /> </> ) } export default PhoneNumberInputClearField
To access the kit's input element attributes or add event listeners, create a ref
inside your parent component, pass it to the kit's ref
prop, and use ref.current.inputNode()
with your desired attribute or event listener inside a useEffect
hook. useEffect
is necessary because the ref
will be initially undefined
. Calling useEffect
with an empty dependency array ensures your event listeners won't be added twice.
inputNode()
is a custom function inside the kit that returns the input DOM element and its attributes. For example, to get the name
attribute, use ref.current.inputNode().name
import React, { useEffect, useRef } from 'react' import { Body, PhoneNumberInput } from 'playbook-ui' const PhoneNumberInputAccessInputElement = (props) => { // 1. Create a ref - this accesses the kit's input element. const ref = useRef() // 2. Add any event listener to ref.current.inputNode() inside a useEffect hook and trigger it once. useEffect(() => { ref.current.inputNode().addEventListener("click", () => alert("Clicked!")) }, []) // 3. Pass the ref to the ref prop. return ( <> <Body text="Click the input field below:" /> <PhoneNumberInput id="access-input-element" ref={ref} /> </> ) } export default PhoneNumberInputAccessInputElement
NOTE: the number
in the React onChange
event will not include formatting (no spaces, dashes, and parentheses). For Rails, the value
will include formatting and its value must be sanitized manually.
import React, { useState } from "react"; import { Body, PhoneNumberInput } from 'playbook-ui' const PhoneNumberInputFormat = (props) => { const [phoneNumber, setPhoneNumber] = useState(""); const handleOnChange = ({ number }) => { setPhoneNumber(number); }; return ( <> <PhoneNumberInput formatAsYouType id="format" onChange={handleOnChange} /> {phoneNumber && <Body>Unformatted number: {phoneNumber}</Body>} </> ); }; export default PhoneNumberInputFormat;
Ignore any irrelevant characters and cap the length at the maximum valid number length.
This can be combined with format_as_you_type
/ formatAsYouType
.
import React from "react"; import { Body, PhoneNumberInput } from 'playbook-ui' const PhoneNumberInputStrictMode = (props) => { return ( <> <PhoneNumberInput id="strict" strictMode /> <Body>With formatAsYouType</Body> <PhoneNumberInput formatAsYouType id="strict" strictMode /> </> ); }; export default PhoneNumberInputStrictMode;
Set country_search
/ countrySearch
to true to allow users to search for a specific Country within the dropdown. If the range of countries has been limited, only the selected countries will be searchable.
import React from 'react' import { PhoneNumberInput } from 'playbook-ui' const PhoneNumberInputCountrySearch = (props) => ( <> <PhoneNumberInput countrySearch id='country-search' /> <PhoneNumberInput countrySearch id='country-search-limited' onlyCountries={["br", "us", "ph", "gb"]} /> </> ) export default PhoneNumberInputCountrySearch
import React, { useState } from 'react' import { Caption, TextInput, Title } from 'playbook-ui' const TextInputDefault = (props) => { const [firstName, setFirstName] = useState('') const handleOnChangeFirstName = ({ target }) => { setFirstName(target.value) } const ref = React.createRef() const [formFields, setFormFields] = useState({ firstName: 'Jane', lastName: 'Doe', phone: '8888888888', email: 'jane@doe.com', zip: 55555, }) const handleOnChangeFormField = ({ target }) => { const { name, value } = target setFormFields({ ...formFields, [name]: value }) } return ( <div> <TextInput aria={{ label: 'hello' }} data={{ say: 'hi', yell: 'go' }} id="unique-id" label="First Name" name="firstName" onChange={handleOnChangeFormField} placeholder="Enter first name" value={formFields.firstName} /> <TextInput label="Last Name" name="lastName" onChange={handleOnChangeFormField} placeholder="Enter last name" value={formFields.lastName} /> <TextInput label="Phone Number" name="phone" onChange={handleOnChangeFormField} placeholder="Enter phone number" type="phone" value={formFields.phone} /> <TextInput label="Email Address" name="email" onChange={handleOnChangeFormField} placeholder="Enter email address" type="email" value={formFields.email} /> <TextInput label="Zip Code" name="zip" onChange={handleOnChangeFormField} placeholder="Enter zip code" type="number" value={formFields.zip} /> <br /> <br /> <Title>{'Event Handler Props'}</Title> <br /> <Caption>{'onChange'}</Caption> <br /> <TextInput label="First Name" onChange={handleOnChangeFirstName} placeholder="Enter first name" ref={ref} value={firstName} /> {firstName !== '' && ( <React.Fragment>{`First name is: ${firstName}`}</React.Fragment> )} </div> ) } export default TextInputDefault
Text Input w/ Error shows that the radio option must be selected or is invalid (ie when used in a form it signals a user to fix an error).
import React, { useState } from 'react' import { TextInput, Icon } from 'playbook-ui' const TextInputError = (props) => { const [email, setEmail] = useState('') const handleUpdateEmail = ({ target }) => { setEmail(target.value) } const error = ( <> <Icon icon="warning" /> Please enter a valid email address </> ) return ( <div> <TextInput addOn={{ icon: 'user', alignment: 'left', border: true }} error={error} label="Email Address" onChange={handleUpdateEmail} placeholder="Enter email address" type="email" value={email} /> <TextInput addOn={{ icon: 'user', alignment: 'left', border: true }} label="Confirm Email Address" onChange={handleUpdateEmail} placeholder="Confirm email address" type="email" value={email} /> </div> ) } export default TextInputError
import React, { useState } from 'react' import { TextInput } from 'playbook-ui' const TextInputCustom = (props) => { const [name, setName] = useState('') const handleUpdateName = ({ target }) => { setName(target.value) } return ( <div> <TextInput label="Custom Label" > <input name="custom-name" onChange={handleUpdateName} placeholder="custom-placeholder" type="text" value={name} /> </TextInput> </div> ) } export default TextInputCustom
import React, { useState } from 'react' import { TextInput } from 'playbook-ui' const TextInputAddOn = (props) => { const [defaultInput, setDefaultInput] = useState('') const [firstInput, setFirstInput] = useState('') const [secondInput, setSecondInput] = useState('') const [thirdInput, setThirdInput] = useState('') const [fourthInput, setFourthInput] = useState('') const handleUpdateDefaultInput = ({ target }) => { setDefaultInput(target.value) } const handleUpdateFirstInput = ({ target }) => { setFirstInput(target.value) } const handleUpdateSecondInput = ({ target }) => { setSecondInput(target.value) } const handleUpdateThirdInput = ({ target }) => { setThirdInput(target.value) } const handleUpdateFourthInput = ({ target }) => { setFourthInput(target.value) } return ( <> <div> <TextInput addOn={{ icon: 'bat' }} label="Add On With Defaults" onChange={handleUpdateDefaultInput} value={defaultInput} /> </div> <div> <TextInput addOn={{ icon: 'user', alignment: 'right', border: true }} label="Right-Aligned Add On With Border" onChange={handleUpdateFirstInput} value={firstInput} /> </div> <div> <TextInput addOn={{ icon: 'percent', alignment: 'right', border: false }} label="Right-Aligned Add On With No Border" onChange={handleUpdateThirdInput} value={thirdInput} /> </div> <div> <TextInput addOn={{ icon: 'frog', alignment: 'right', border: true }} label="Right-Aligned Add On With Child Input" onChange={handleUpdateFourthInput} > <input /> </TextInput> </div> <div> <TextInput addOn={{ icon: 'percent', alignment: 'left', border: false }} label="Left-Aligned Add On With No Border" onChange={handleUpdateSecondInput} value={secondInput} /> </div> <div> <TextInput addOn={{ icon: 'percent', alignment: 'left', border: true }} label="Left-Aligned Add On With Border" onChange={handleUpdateFourthInput} value={fourthInput} /> </div> <div> <TextInput addOn={{ icon: 'frog', alignment: 'left', border: true }} label="Left-Aligned Add On With Child Input" onChange={handleUpdateFourthInput} > <input /> </TextInput> </div> </> ) } export default TextInputAddOn
import React, { useState } from 'react' import { TextInput } from 'playbook-ui' const TextInputInline = (props) => { const [value, setValue] = useState('Inline Input') const handleValueChange = ({ target }) => { setValue(target.value) } return ( <div> <TextInput inline label="Hover Over Text Below" onChange={handleValueChange} value={value} /> </div> ) } export default TextInputInline
import React, { useState } from 'react' import { TextInput } from 'playbook-ui' const TextInputNoLabel = (props) => { const [email, setEmail] = useState('') const handleUpdateEmail = ({ target }) => { setEmail(target.value) } return ( <div> <TextInput onChange={handleUpdateEmail} placeholder="Enter email address" type="email" value={email} /> </div> ) } export default TextInputNoLabel
import React, { useState } from 'react' import { Caption, TextInput, Title } from 'playbook-ui' const TextInputMask = (props) => { const [ssn, setSSN] = useState('') const handleOnChangeSSN = ({ target }) => { setSSN(target.value) } const ref = React.createRef() const [formFields, setFormFields] = useState({ currency: '', zipCode: '', postalCode: '', ssn: '', creditCard: '', cvv: '' }) const handleOnChangeFormField = ({ target }) => { const { name, value } = target setFormFields({ ...formFields, [name]: value }) } return ( <div> <TextInput label="Currency" mask="currency" name="currency" onChange={handleOnChangeFormField} value={formFields.currency} /> <TextInput label="Zip Code" mask="zipCode" name="zipCode" onChange={handleOnChangeFormField} value={formFields.zipCode} /> <TextInput label="Postal Code" mask="postalCode" name="postalCode" onChange={handleOnChangeFormField} value={formFields.postalCode} /> <TextInput label="SSN" mask="ssn" name="ssn" onChange={handleOnChangeFormField} value={formFields.ssn} /> <TextInput label="Credit Card" mask="creditCard" name="creditCard" onChange={handleOnChangeFormField} value={formFields.creditCard} /> <TextInput label="CVV" mask="cvv" name="cvv" onChange={handleOnChangeFormField} value={formFields.cvv} /> <br /> <br /> <Title>{'Event Handler Props'}</Title> <br /> <Caption>{'onChange'}</Caption> <br /> <TextInput label="SSN" mask="ssn" onChange={handleOnChangeSSN} placeholder="Enter SSN" ref={ref} value={ssn} /> {ssn !== '' && ( <React.Fragment>{`SSN is: ${ssn}`}</React.Fragment> )} </div> ) } export default TextInputMask
When utilizing the Mask prop, you can retrieve the sanitized value of your input through an event handler that targets sanitizedValue
.
import React, { useState } from 'react' import { TextInput } from 'playbook-ui' const TextInputSanitize = (props) => { const ref = React.createRef() const [currency, setCurrency] = useState('') const [sanitizedCurrency, setSanitizedCurrency] = useState('') const handleOnChangeSanitizeCurrency = ({ target }, sanitizedValue) => { setCurrency(target.value) setSanitizedCurrency(sanitizedValue); } const [creditCard, setCreditCard] = useState('') const [sanitizedCreditCard, setSanitizedCreditCard] = useState('') const handleOnChangeSanitizeCC = ({ target }, sanitizedValue) => { setCreditCard(target.value) setSanitizedCreditCard(sanitizedValue); } const [ssn, setSSN] = useState('') const [sanitizedSSN, setSanitizedSSN] = useState('') const handleOnChangeSanitizeSSN = ({ target }, sanitizedValue) => { setSSN(target.value) setSanitizedSSN(sanitizedValue); } return ( <div> <TextInput label="Currency" mask="currency" onChange={handleOnChangeSanitizeCurrency} placeholder="Enter Amount" ref={ref} value={currency} /> {currency !== "" && ( <React.Fragment>{`The masked value is: ${currency}`}</React.Fragment> )} <br /> {sanitizedCurrency !== "" && ( <React.Fragment>{`The sanitized value is: ${sanitizedCurrency}`}</React.Fragment> )} <br /> <br /> <TextInput label="Credit Card" mask="creditCard" onChange={handleOnChangeSanitizeCC} placeholder="Enter Card" ref={ref} value={creditCard} /> {creditCard !== "" && ( <React.Fragment>{`The masked value is: ${creditCard}`}</React.Fragment> )} <br /> {sanitizedCreditCard !== "" && ( <React.Fragment>{`The sanitized value is: ${sanitizedCreditCard}`}</React.Fragment> )} <br /> <br /> <TextInput label="SSN" mask="ssn" onChange={handleOnChangeSanitizeSSN} placeholder="Enter Amount" ref={ref} value={ssn} /> {ssn !== "" && ( <React.Fragment>{`The masked value is: ${ssn}`}</React.Fragment> )} <br /> {sanitizedSSN !== "" && ( <React.Fragment>{`The sanitized value is: ${sanitizedSSN}`}</React.Fragment> )} </div> ) } export default TextInputSanitize
import React, { useState } from 'react' import { RichTextEditor } from 'playbook-ui' const RichTextEditorDefault = (props) => { const [value, setValue] = useState('Add your text here. You can format your text, add links, quotes, and bullets.'), handleOnChange = (html) => setValue(html) return ( <div> <RichTextEditor onChange={handleOnChange} value={value} /> </div> ) } export default RichTextEditorDefault
The advanced variant leverages Tiptap to unlock additional UI options (e.g., consolidated nav toolbar, styling, etc.) as well as several extensions (see Tiptap docs for more). To leverage this variant, Tiptap must be installed in your project. Complete docs for using the library can be found here. To get started with this variant, see the Code Example below for required imports as well as the basic setup.
NOTE: Once the Tiptap editor is initialized as shown below, you must pass that instance to the kit via the advancedEditor
prop.
import React from 'react' import { RichTextEditor } from 'playbook-ui' import { useEditor, EditorContent } from "@tiptap/react" import StarterKit from "@tiptap/starter-kit" import Link from '@tiptap/extension-link' const RichTextEditorAdvancedDefault = (props) => { const editor = useEditor({ extensions: [ StarterKit, Link ], content:"Add your text here. You can format your text, add links, quotes, and bullets." }) if (!editor) { return null } return ( <div> <RichTextEditor advancedEditor={editor} > <EditorContent editor={editor}/> </RichTextEditor> </div> ) } export default RichTextEditorAdvancedDefault
This variant allows you to optionally include any of Tiptap’s 53 extensions within any advanced editor by using the extensions
prop.
NOTE: In order to leverage this prop, you must install the extension you need in your project, import it and pass it to the extensions array as shown in this example with the HorizontalRule and the Highlight extensions.
In order to add the extension to the editor toolbar, create an array of objects (as shown in the ExtensionsList array in the example below). Each object in this array should include:
icon
: the icon to display within the toolbar dropdown (any Fontawesome icons can be used)
isActive
: sets the extension to active within the dropdown, when applicable
text
: the label within the toolbar dropdown
onclick
: initializes the extension when it’s clicked within the dropdown (snytax varies with extension, see Tiptap's docs for more information)
This array can then be passed to the extensions
prop and all extensions in the array will be rendered in the ellipsis dropdown.
import React from 'react' import { RichTextEditor } from 'playbook-ui' import { useEditor, EditorContent } from "@tiptap/react" import StarterKit from "@tiptap/starter-kit" import Link from '@tiptap/extension-link' import HorizontalRule from "@tiptap/extension-horizontal-rule" import Highlight from '@tiptap/extension-highlight' const RichTextEditorMoreExtensions = (props) => { const editor = useEditor({ extensions: [ StarterKit, Link, HorizontalRule, Highlight.configure({ multicolor: true }) ], content:"Add your text here. You can format your text, add links, quotes, and bullets." }) if (!editor) { return null } const ExtensionsList = [ { icon: "horizontal-rule", isActive: editor.isActive("horizontalRule"), text: "Horizontal Rule", onclick: () => editor.chain().focus().setHorizontalRule().run(), }, { icon: "highlighter", isActive: editor.isActive("highlight"), text: "Highlighter", onclick: () => editor.chain().focus().toggleHighlight().run(), } ] return ( <div> <RichTextEditor advancedEditor={editor} extensions={ExtensionsList} > <EditorContent editor={editor}/> </RichTextEditor> </div> ) } export default RichTextEditorMoreExtensions
Because our default variant's toolbar requires Tiptap's StarterKit which may include features that are not relevant to your project or even block some custom extensions, optionally setting advancedEditorToolbar
to false
creates an editor without a toolbar, using only the minimum requirements.
NOTE: Omitting the Starter Kit requires that the editor’s default extensions (document
, paragraph
, and text
) must be imported directly from Tiptap or as a custom extensions.
import React from "react"; import { RichTextEditor } from 'playbook-ui'import { useEditor, EditorContent } from "@tiptap/react"; import Document from "@tiptap/extension-document"; import Paragraph from "@tiptap/extension-paragraph"; import Text from "@tiptap/extension-text"; const RichTextEditorToolbarDisabled = (props) => { const editor = useEditor({ extensions: [Document, Paragraph, Text], content: "Add your text here. You can format your text, add links, quotes, and bullets.", }); if (!editor) { return null; } return ( <div> <RichTextEditor advancedEditor={editor} advancedEditorToolbar={false} > <EditorContent editor={editor} /> </RichTextEditor> </div> ); }; export default RichTextEditorToolbarDisabled;
import React from 'react' import { RichTextEditor } from 'playbook-ui' const RichTextEditorSticky = (props) => ( <div> <RichTextEditor id="sticky" sticky value="In this example, when you scroll down, the rich text editor's toolbar will scroll along with the page and it will no longer be visible at the top of the page. Dummy text to enable scroll.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ornare lorem ut pellentesque tempor. Vivamus ut ex vestibulum velit rich text editor eleifend fringilla. Sed non metus dictum, elementum mauris wysiwyg html editor non, sagittis odio. Nullam pellentesque leo sit amet ante suscipit wysiwyg html editor sagittis. Donec tempus vulputate suscipit. Ut non felis rich text editor ac dolor pulvinar lacinia eu eget urna. Sed tincidunt sapien vulputate tellus fringilla sodales. Morbi accumsan dui wysiwyg html editor sed massa pellentesque, quis vestibulum lectus scelerisque. Nulla ultrices mi id felis luctus aliquet. Donec nec ligula wysiwyg html editor pretium sapien semper dictum eu id quam. Etiam ut sollicitudin nibh. Quisque eu ultrices dui. Nunc rich text editor congue, enim vitae dictum dignissim, libero nisl sagittis augue, non aliquet nibh tortor sit amet ex. Aliquam cursus maximus rich text editor mi eu consequat. Nullam tincidunt erat et placerat mattis. Nunc rich text editor congue, enim vitae dictum dignissim, libero nisl sagittis augue, non aliquet nibh tortor sit amet ex. Aliquam cursus maximus mi eu consequat. Nullam tincidunt erat et placerat mattis.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ornare lorem ut pellentesque tempor. Vivamus ut ex vestibulum velit rich text editor eleifend fringilla. Sed non metus dictum, elementum mauris wysiwyg html editor non, sagittis odio. Nullam pellentesque leo sit amet ante suscipit wysiwyg html editor sagittis. Donec tempus vulputate suscipit. Ut non felis rich text editor ac dolor pulvinar lacinia eu eget urna. Sed tincidunt sapien vulputate tellus fringilla sodales. Morbi accumsan dui wysiwyg html editor sed massa pellentesque, quis vestibulum lectus scelerisque. Nulla ultrices mi id felis luctus aliquet. Donec nec ligula wysiwyg html editor pretium sapien semper dictum eu id quam. Etiam ut sollicitudin nibh. Quisque eu ultrices dui. Nunc rich text editor congue, enim vitae dictum dignissim, libero nisl sagittis augue, non aliquet nibh tortor vulputate suscipit. Ut non felis rich text editor ac dolor pulvinar lacinia eu eget urna. Sed tincidunt sapien vulputate tellus fringilla sodales. Morbi accumsan dui wysiwyg html editor sed massa pellentesque, quis vestibulum lectus scelerisque. Nulla ultrices mi id felis luctus aliquet. Donec nec ligula wysiwyg html editor pretium sapien semper dictum eu id quam. Etiam ut sollicitudin nibh. Quisque eu ultrices dui. Nunc rich text editor congue, enim vitae dictum dignissim, libero nisl sagittis augue, non aliquet nibh tortor sit amet ex. Aliquam cursus maximus rich text editor mi eu consequat. Nullam tincidunt erat et placerat mattis. Nunc rich text editor congue, enim vitae dictum dignissim, libero nisl sagittis augue, non aliquet nibh tortor sit amet ex. Aliquam cursus maximus mi eu consequat. Nullam tincidunt erat et placerat mattis.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ornare lorem ut pellentesque tempor. Vivamus ut ex vestibulum velit rich text editor eleifend fringilla. Sed non metus dictum, elementum mauris wysiwyg html editor non, sagittis odio. Nullam pellentesque leo sit amet ante suscipit wysiwyg html editor sagittis. Donec tempus vulputate suscipit. Ut non felis rich text editor ac dolor pulvinar lacinia eu eget urna. Sed tincidunt sapien vulputate tellus fringilla sodales. Morbi accumsan dui wysiwyg html editor sed massa pellentesque, quis vestibulum lectus scelerisque. Nulla ultrices mi id felis luctus aliquet. Donec nec ligula wysiwyg html editor pretium sapien semper dictum eu id quam. Etiam ut sollicitudin nibh. Quisque eu ultrices dui. Nunc rich text editor congue, enim vitae dictum dignissim, libero nisl sagittis augue, non aliquet nibh tortor sit amet ex. Aliquam cursus maximus rich text editor mi eu consequat. Nullam tincidunt erat et placerat mattis. Nunc rich text editor congue, enim vitae dictum dignissim, libero nisl sagittis augue, non aliquet nibh tortor sit amet ex. Aliquam cursus maximus mi eu consequat. Nullam tincidunt erat et placerat mattis.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ornare lorem ut pellentesque tempor. Vivamus ut ex vestibulum velit rich text editor eleifend fringilla. Sed non metus dictum, elementum mauris wysiwyg html editor non, sagittis odio. Nullam pellentesque leo sit amet ante suscipit wysiwyg html editor sagittis. Donec tempus vulputate suscipit. Ut non felis rich text editor ac dolor pulvinar lacinia eu eget urna. Sed tincidunt sapien vulputate tellus fringilla sodales. Morbi accumsan dui wysiwyg html editor sed massa pellentesque, quis vestibulum lectus scelerisque. Nulla ultrices mi id felis luctus aliquet. Donec nec ligula wysiwyg html editor pretium sapien semper dictum eu id quam. Etiam ut sollicitudin nibh. Quisque eu ultrices dui. Nunc rich text editor congue, enim vitae dictum dignissim, libero nisl sagittis augue, non aliquet nibh tortor sit amet ex. Aliquam cursus maximus rich text editor mi eu consequat. Nullam tincidunt erat et placerat mattis. Nunc rich text editor congue, enim vitae dictum dignissim, libero nisl sagittis augue, non aliquet nibh tortor sit amet ex. Aliquam cursus maximus mi eu consequat. Nullam tincidunt erat et placerat mattis.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ornare lorem ut pellentesque tempor. Vivamus ut ex vestibulum velit rich text editor eleifend fringilla. Sed non metus dictum, elementum mauris wysiwyg html editor non, sagittis odio. Nullam pellentesque leo sit amet ante suscipit wysiwyg html editor sagittis. Donec tempus vulputate suscipit. Ut non felis rich text editor ac dolor pulvinar lacinia eu eget urna. Sed tincidunt sapien vulputate tellus fringilla sodales. Morbi accumsan dui wysiwyg html editor sed massa pellentesque, quis vestibulum lectus scelerisque. Nulla ultrices mi id felis luctus aliquet. Donec nec ligula wysiwyg html editor pretium sapien semper dictum eu id quam. Etiam ut sollicitudin nibh. Quisque eu ultrices dui. Nunc rich text editor congue, enim vitae dictum dignissim, libero nisl sagittis augue, non aliquet nibh tortor sit amet ex. Aliquam cursus maximus rich text editor mi eu consequat. Nullam tincidunt erat et placerat mattis. Nunc rich text editor congue, enim vitae dictum dignissim, libero nisl sagittis augue, non aliquet nibh tortor sit amet ex. Aliquam cursus maximus mi eu consequat. Nullam tincidunt erat et placerat mattis.sit amet ex. Aliquam cursus maximus rich text editor mi eu consequat. Nullam tincidunt erat et placerat mattis. Nunc rich text editor congue, enim vitae dictum dignissim, libero nisl sagittis augue, non aliquet nibh tortor sit amet ex. Aliquam cursus maximus mi eu consequat. Nullam tincidunt erat et placerat mattis.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ornare lorem ut pellentesque tempor. Vivamus ut ex vestibulum velit rich text editor eleifend fringilla. Sed non metus dictum, elementum mauris wysiwyg html editor non, sagittis odio. Nullam pellentesque leo sit amet ante suscipit wysiwyg html editor sagittis. Donec tempus vulputate suscipit. Ut non felis rich text editor ac dolor pulvinar lacinia eu eget urna. Sed tincidunt sapien vulputate tellus fringilla sodales. Morbi accumsan dui wysiwyg html editor sed massa pellentesque, quis vestibulum lectus scelerisque. Nulla ultrices mi id felis luctus aliquet. Donec nec ligula wysiwyg html editor pretium sapien semper dictum eu id quam. Etiam ut sollicitudin nibh. Quisque eu ultrices dui. Nunc rich text editor congue, enim vitae dictum dignissim, libero nisl sagittis augue, non aliquet nibh tortor sit amet ex. Aliquam cursus maximus rich text editor mi eu consequat. Nullam tincidunt erat et placerat mattis. Nunc rich text editor congue, enim vitae dictum dignissim, libero nisl sagittis augue, non aliquet nibh tortor sit amet ex. Aliquam cursus maximus mi eu consequat. Nullam tincidunt erat et placerat mattis.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ornare lorem ut pellentesque tempor. Vivamus ut ex vestibulum velit rich text editor eleifend fringilla. Sed non metus dictum, elementum mauris wysiwyg html editor non, sagittis odio. Nullam pellentesque leo sit amet ante suscipit wysiwyg html editor sagittis. Donec tempus vulputate suscipit. Ut non felis rich text editor ac dolor pulvinar lacinia eu eget urna. Sed tincidunt sapien vulputate tellus fringilla sodales. Morbi accumsan dui wysiwyg html editor sed massa pellentesque, quis vestibulum lectus scelerisque. Nulla ultrices mi id felis luctus aliquet. Donec nec ligula wysiwyg html editor pretium sapien semper dictum eu id quam. Etiam ut sollicitudin nibh. Quisque eu ultrices dui. Nunc rich text editor congue, enim vitae dictum dignissim, libero nisl sagittis augue, non aliquet nibh tortor sit amet ex. Aliquam cursus maximus rich text editor mi eu consequat. Nullam tincidunt erat et placerat mattis. Nunc rich text editor congue, enim vitae dictum dignissim, libero nisl sagittis augue, non aliquet nibh tortor sit amet ex. Aliquam cursus maximus mi eu consequat. Nullam tincidunt erat et placerat mattis." /> </div> ) export default RichTextEditorSticky
import React, { useState } from 'react' import { RichTextEditor, Select } from 'playbook-ui' import { changelog, release } from './templates.js' const RichTextEditorTemplates = (props) => { const [editorContent, setEditorContent] = useState('') const handleChange = (event) => { setEditorContent(event.target.value) } const options = [ { value: release, text: 'Playbook Release', }, { value: changelog, text: 'Changelog', }, ] return ( <div> <Select blankSelection="Select a template..." label="Template" onChange={handleChange} options={options} /> <RichTextEditor id="template" template={editorContent} /> </div> ) } export default RichTextEditorTemplates
import React from 'react' import { RichTextEditor } from 'playbook-ui' const RichTextEditorInline = (props) => ( <div> <RichTextEditor id="inline" inline toolbarBottom value="Try hovering over this text. Then try modifying it or adding more of your own text." /> </div> ) export default RichTextEditorInline
import React, { useState } from 'react' import { RichTextEditor, Button, Card } from 'playbook-ui' const RichTextEditorPreview = (props) => { const [showPreview, setShowPreview] = useState(false) const [previewText, setPreviewText] = useState(<div />) const handleChange = (event) => setPreviewText(event) const handleClick = () => { setShowPreview(true) } return ( <div> <RichTextEditor id="content-preview-editor" onChange={handleChange} /> {showPreview && ( <Card marginTop="md"> <div className="trix-content" // eslint-disable-next-line react/no-danger dangerouslySetInnerHTML={{ __html: previewText }} id="preview-content" /> </Card> )} {!showPreview && ( <div /> )} <Button id="preview-button" marginTop="md" onClick={handleClick} text="Preview Output" variant="secondary" /> </div> ) } export default RichTextEditorPreview
import React, { useState } from 'react' import { RichTextEditor, Button, Card } from 'playbook-ui' import { useEditor, EditorContent } from "@tiptap/react" import StarterKit from "@tiptap/starter-kit" import Link from '@tiptap/extension-link' const RichTextEditorAdvancedPreview = (props) => { const editor = useEditor({ extensions: [ StarterKit, Link ], content: "Add text here, format it, and press \"Preview Output\" to see what your stylized output will look like on the page." }) const [showPreview, setShowPreview] = useState(false) const [previewText, setPreviewText] = useState(<div />) const handleChange = () => { if (editor) { setPreviewText(editor.getHTML()) } } const handleClick = () => { handleChange() setShowPreview(true) } if (!editor) { return null } return ( <div> <RichTextEditor advancedEditor={editor} id="content-advanced-preview-editor" onChange={handleChange} > <EditorContent editor={editor}/> </RichTextEditor> {showPreview && ( <Card marginTop="md" maxWidth="md" > <div className="tiptap-content" // eslint-disable-next-line react/no-danger dangerouslySetInnerHTML={{ __html: previewText }} id="advanced-preview-content" /> </Card> )} {!showPreview && ( <div /> )} <Button id="preview-button" marginTop="md" onClick={handleClick} text="Preview Output" variant="secondary" /> </div> ) } export default RichTextEditorAdvancedPreview
import React, {useState} from 'react' import { Textarea } from 'playbook-ui' const TextareaDefault = (props) => { const [value, setValue] = useState('Default value text') const handleChange = (event) => { setValue(event.target.value) } return ( <div> <Textarea label="Label" rows={4} /> <br /> <Textarea label="Label" placeholder="Placeholder text" /> <br /> <Textarea label="Label" name="comment" onChange={(e) => handleChange(e)} placeholder="Placeholder text" value={value} /> </div> ) } export default TextareaDefault
import React from 'react' import { Textarea } from 'playbook-ui' const TextareaCustom = (props) => { return ( <div> <Textarea label="Label" > <textarea className="my_custom_class" name="custom_textarea" rows={4} > {'Content goes here.'} </textarea> </Textarea> </div> ) } export default TextareaCustom
import React from 'react' import { Textarea } from 'playbook-ui' const TextareaResize = (props) => { return ( <div> <Textarea label="auto" placeholder="Resize Auto" resize="auto" /> <br /> <Textarea label="vertical" placeholder="Resize Vertical" resize="vertical" /> <br /> <Textarea label="both" placeholder="Resize Both" resize="both" /> <br /> <Textarea label="horizontal" placeholder="Resize Horizontal" resize="horizontal" /> </div> ) } export default TextareaResize
Textarea w/ Error shows that the radio option must be selected or is invalid (ie when used in a form it signals a user to fix an error).
import React, {useState} from 'react' import { Textarea, Icon } from 'playbook-ui' const TextareaError = (props) => { const [value, setValue] = useState('default value text') const handleChange = (event) => { setValue(event.target.value) } const error = ( <> <Icon icon="warning" /> This field has an error! </> ) return ( <div> <Textarea error={error} label="Label" name="comment" onChange={(e)=> handleChange(e)} placeholder="Placeholder text" value={value} /> </div> ) } export default TextareaError
import React, { useState } from 'react' import { Textarea } from 'playbook-ui' const TextareaCharacterCounter = (props) => { const [value1, setValue1] = useState('Counting characters!') const [value2, setValue2] = useState('This counter prevents the user from exceeding the maximum number of allowed characters. Just try it!') const [value3, setValue3] = useState('This counter alerts the user that they have exceeded the maximum number of allowed characters.') const [error, setError] = useState('Too many characters!') const [count1, setCount1] = useState(0) const [count2, setCount2] = useState(value1.length) const [count3, setCount3] = useState(value2.length) const [count4, setCount4] = useState(value3.length) const handleMaxCount = (event) => { setCount2(event.target.value.length) setValue1(event.target.value) } const handleMaxCountWithBlocker = (event, maxCharacters) => { if (event.target.value.length <= maxCharacters) { setCount3(event.target.value.length) setValue2(event.target.value) } } const handleMaxCountWithError = (event, maxCharacters) => { if (event.target.value.length > maxCharacters) { setError('Too many characters!') } else { setError('') } setCount4(event.target.value.length) setValue3(event.target.value) } return ( <> <Textarea characterCount={count1} label="Count Only" onChange={(event) => setCount1(event.target.value.length)} rows={4} /> <br /> <Textarea characterCount={count2} label="Max Characters" maxCharacters="100" onChange={() => handleMaxCount(event)} rows={4} value={value1} /> <br /> <Textarea characterCount={count3} label="Max Characters w/ Blocker" maxCharacters="100" onChange={() => handleMaxCountWithBlocker(event, 100)} rows={4} value={value2} /> <br /> <Textarea characterCount={count4} error={error} label="Max Characters w/ Error" maxCharacters="75" onChange={() => handleMaxCountWithError(event, 75)} rows={4} value={value3} /> </> ) } export default TextareaCharacterCounter
import React, { useState } from 'react' import { Textarea } from 'playbook-ui' const TextareaInline = (props) => { const [value, setValue] = useState('Try clicking into this text.') const handleChange = (event) => { setValue(event.target.value) } return ( <div> <Textarea inline onChange={(e) => handleChange(e)} resize="auto" rows={1} value={value} /> </div> ) } export default TextareaInline
import React from 'react' import { Typeahead } from 'playbook-ui' const options = [ { label: 'Orange', value: '#FFA500' }, { label: 'Red', value: '#FF0000' }, { label: 'Green', value: '#00FF00' }, { label: 'Blue', value: '#0000FF' }, ] const TypeaheadDefault = (props) => { return ( <Typeahead label="Colors" options={options} /> ) } export default TypeaheadDefault
You can pass react-hook-form
props to the Typeahead kit.
import React from 'react' import { Typeahead, Title } from 'playbook-ui' import { useForm } from 'react-hook-form' const languages = [ { label: 'JavaScript', value: '1995', category: 'Web Development' }, { label: 'Python', value: '1991', category: 'General Purpose' }, { label: 'Java', value: '1995', category: 'Enterprise' }, { label: 'C++', value: '1985', category: 'Systems Programming' }, { label: 'Go', value: '2009', category: 'Systems Programming' }, { label: 'Rust', value: '2010', category: 'Systems Programming' }, { label: 'Swift', value: '2014', category: 'Mobile Development' }, { label: 'Kotlin', value: '2011', category: 'Mobile Development' }, { label: 'Ruby', value: '1995', category: 'General Purpose' }, { label: 'PHP', value: '1995', category: 'Web Development' }, ] const colors = [ { label: 'Orange', value: '#FFA500' }, { label: 'Red', value: '#FF0000' }, { label: 'Green', value: '#00FF00' }, { label: 'Blue', value: '#0000FF' }, ] const TypeaheadReactHook = (props) => { const { register, watch } = useForm() const selectedLanguages = watch('languages') const selectedColor = watch('color') return ( <> <Typeahead isMulti label="Multi Select Languages" multiKit="language" options={languages} {...register('languages')} /> <Title size={4} text='Selected Languages' /> {selectedLanguages && selectedLanguages.map(language => ( <p key={language.label}>{`${language.label} - ${language.value} - ${language.category}`}</p> ))} <Typeahead label="Colors" marginTop="lg" options={colors} {...register('color')} /> <Title size={4} text='Selected Color' /> <p>{ selectedColor && `${selectedColor.label} - ${selectedColor.value}`}</p> </> ) } export default TypeaheadReactHook
/* eslint-disable react/no-danger */ /* eslint-disable react/no-multi-comp */ import React, { useState } from 'react' import { Avatar, Body, Flex, FlexItem, Title, Typeahead } from 'playbook-ui'import { components } from 'react-select' const USERS = [ { name: "Wade Winningham", title: "Nitro Principal Developer", territory: "PHL", }, { name: "Carlos Lima", title: "Nitro Developer", territory: "PHL", }, { name: "Stephen Marshall", title: "Senior Nitro Developer", territory: "PHL", }, { name: "Jasper Furniss", title: "Lead User Experience Engineer", territory: "PHL", }, ]; const TypeaheadWithHighlight = (props) => { const [selectedUser, setSelectedUser] = useState() const formatOptionLabel = ({name, territory, title}, {inputValue}) => { const highlighted = (text) => { if (!inputValue.length) return text return text.replace( new RegExp(inputValue, 'gi'), (highlighted) => `<mark>${highlighted}</mark>` ) } return ( <Flex> <FlexItem> <Avatar marginRight="sm" name={name} size="sm" /> </FlexItem> <FlexItem> <Title size={4} > <span dangerouslySetInnerHTML={{ __html: highlighted(name) }} /></Title> <Body color="light" > <span dangerouslySetInnerHTML={{ __html: highlighted(title) }} />{" • "} {territory} </Body> </FlexItem> </Flex> ) } const customComponents = { Option: (highlightProps) => ( <components.Option {...highlightProps}/> ), SingleValue: ({ ...props }) => ( <components.SingleValue> <span>{props.data.name}</span> </components.SingleValue> ) } return ( <React.Fragment> <Typeahead components={customComponents} formatOptionLabel={formatOptionLabel} getOptionLabel={(option) => option.name} getOptionValue={({name, title}) => `${name} ${title}`} label="Users" onChange={(user) => setSelectedUser(user)} options={USERS.filter((option) => option.name != selectedUser?.name)} placeholder="type the name of a user" /> </React.Fragment> ) } export default TypeaheadWithHighlight
Typeahead kit is data-driven. The minimum default fields are label
and value
.
This is an example of an option: { label: 'Windows', value: '#FFA500' }
You can also pass default_options
which will populate the initial pill selections:
default_options: [{ label: 'Windows', value: '#FFA500' }]
JavaScript events are triggered based on actions you take within the kit such as selection, removal and clearing.
This kit utilizes a default id
prop named react-select-input
. It is highly advised to send your own unique id
prop when using this kit to ensure these events do not unintentionally affect other instances of the kit in the same view. The examples below will use the unique id
prop named typeahead-pills-example1
:
pb-typeahead-kit-typeahead-pills-example1-result-option-select
event to perform custom work when an option is clicked.
pb-typeahead-kit-typeahead-pills-example1-result-option-remove
event to perform custom work when a pill is clicked.
pb-typeahead-kit-typeahead-pills-example1-result-option-clear
event to perform custom work when all pills are removed upon clicking the X.
The same rule regarding the id
prop applies to publishing JS events. The examples below will use the unique id
prop named typeahead-pills-example1
:
pb-typeahead-kit-typeahead-pills-example1:clear
event to clear all options.
import React from 'react' import { Typeahead } from 'playbook-ui' const options = [ { label: 'Windows', value: '#FFA500' }, { label: 'Siding', value: '#FF0000' }, { label: 'Doors', value: '#00FF00' }, { label: 'Roofs', value: '#0000FF' }, ] const TypeaheadWithPills = (props) => { return ( <> <Typeahead isMulti label="Colors" options={options} placeholder="" /> </> ) } export default TypeaheadWithPills
load_options
Promise *Additional required props: * async: true
, pills: true
The prop load_options
, when used in conjunction with async: true
and pills: true
, points to a JavaScript function located within the global window namespace. This function should return a Promise
which resolves with the list of formatted options as described in prior examples above. This function is identical to the function provided to the React version of this kit. See the code example for more details.
loadOptions
*Additional required props: * async: true
As outlined in the react-select Async docs, loadOptions
expects to return a Promise that resolves resolves with the list of formatted options as described in prior examples above. See the code example for more details.
getOptionLabel
+ getOptionValue
If your server returns data that requires differing field names other than label
and value
See react-select
docs for more information: https://react-select.com/advanced#replacing-builtins
import React, { useState } from 'react' import { Caption, Typeahead, User } from 'playbook-ui' /** * * @const filterResults * @ignore * @returns {[Object]} - a custom mapping of objects, minimally containing * `value` and `label` among other possible fields * @summary - for doc example purposes only */ const filterResults = (results) => results.items.map((result) => { return { name: result.login, id: result.id, } }) /** * * @const promiseOptions * @ignore * @returns {Promise} - fetch API data results from Typeahead input text * @see - https://react-select.com/home#async * @summary - for doc example purposes only */ const promiseOptions = (inputValue) => new Promise((resolve) => { if (inputValue) { fetch(`https://api.github.com/search/users?q=${inputValue}`) .then((response) => response.json()) .then((results) => resolve(filterResults(results))) } else { resolve([]) } }) const TypeaheadWithPillsAsync = (props) => { const [users, setUsers] = useState([]) const formatUsers = (users) => { const results = () => (users.map((user) => { if (Object.keys(user)[0] === 'name' || Object.keys(user)[1] === 'id'){ return ({ label: user.name, value: user.id }) } else { return user } })) return results() } const handleOnChange = (value) => setUsers(formatUsers(value)) const formatValue = (users) => formatUsers(users) return ( <> {users && users.length > 0 && ( <React.Fragment> <Caption marginBottom="xs" text="State (Users)" /> {users.map((user) => ( <User align="left" key={user.value} marginBottom="md" name={user.label} orientation="horizontal" /> ))} </React.Fragment> )} <Typeahead async getOptionLabel={(option) => option.name} getOptionValue={(option) => option.id} isMulti label="Github Users" loadOptions={promiseOptions} onChange={handleOnChange} placeholder="type the name of a Github user" value={formatValue(users)} /> </> ) } export default TypeaheadWithPillsAsync
If the data field imageUrl
is present, FormPill will receive that field as a prop and display the image.
import React, { useState } from 'react' import { Caption, Typeahead, User } from 'playbook-ui' /** * * @const filterResults * @ignore * @returns {[Object]} - a custom mapping of objects, minimally containing * `value` and `label` among other possible fields * @summary - for doc example purposes only */ const filterResults = (results) => results.items.map((result) => { return { imageUrl: result.avatar_url, //add the custom field label: result.login, value: result.id, } }) /** * * @const promiseOptions * @ignore * @returns {Promise} - fetch API data results from Typeahead input text * @see - https://react-select.com/home#async * @summary - for doc example purposes only */ const promiseOptions = (inputValue) => new Promise((resolve) => { if (inputValue) { fetch(`https://api.github.com/search/users?q=${inputValue}`) .then((response) => response.json()) .then((results) => resolve(filterResults(results))) } else { resolve([]) } }) const TypeaheadWithPillsAsyncUsers = (props) => { const [users, setUsers] = useState([]) const handleOnChange = (value) => setUsers(value) /** * * @const handleOnMultiValueClick {function} - a custom callback for the MultiValue click * @ignore * @returns {null} * @summary - for doc example purposes only */ const handleOnMultiValueClick = (value) => { alert(`You added the user: "${value.label}"`) } return ( <> {users && users.length > 0 && ( <React.Fragment> <Caption marginBottom="xs" text="State (Users)" /> {users.map((user) => ( <User align="left" avatar avatarUrl={user.imageUrl} key={user.value} marginBottom="md" name={user.label} orientation="horizontal" /> ))} </React.Fragment> )} <Typeahead async isMulti label="Github Users" loadOptions={promiseOptions} noOptionsMessage={() => 'Type to Search'} onChange={handleOnChange} onMultiValueClick={handleOnMultiValueClick} placeholder="type the name of a Github user" /> </> ) } export default TypeaheadWithPillsAsyncUsers
Use valueComponent
props to pass your desire custom options. valueComponent
will be displayed if present.
import React, { useState } from 'react' import { Caption, Typeahead, User } from 'playbook-ui' /** * * @const filterResults * @ignore * @returns {[Object]} - a custom mapping of objects, minimally containing * `value` and `label` among other possible fields * @summary - for doc example purposes only */ const filterResults = (results) => results.items.map((result) => { return { imageUrl: result.avatar_url, //add the custom field label: result.login, value: result.id, territory: 'PHL', type: result.type, } }) const promiseOptions = (inputValue) => new Promise((resolve) => { if (inputValue) { fetch(`https://api.github.com/search/users?q=${inputValue}`) .then((response) => response.json()) .then((results) => resolve(filterResults(results))) } else { resolve([]) } }) const TypeaheadWithPillsAsyncCustomOptions = (props) => { const [users, setUsers] = useState([]) const handleOnChange = (value) => setUsers(value) /** * * @const handleOnMultiValueClick {function} - a custom callback for the MultiValue click * @ignore * @returns {null} * @summary - for doc example purposes only */ const handleOnMultiValueClick = (value) => { alert(`You added the user: "${value.label}"`) } return ( <> {users && users.length > 0 && ( <React.Fragment> <Caption marginBottom="xs" text="State (Users)" /> {users.map((user) => ( <User align="left" avatar avatarUrl={user.imageUrl} key={user.value} marginBottom="md" name={user.label} orientation="horizontal" /> ))} </React.Fragment> )} <Typeahead async isMulti label="Github Users" loadOptions={promiseOptions} onChange={handleOnChange} onMultiValueClick={handleOnMultiValueClick} placeholder="type the name of a Github user" valueComponent={({imageUrl, label, territory, type}) => ( <User avatar avatarUrl={imageUrl} name={label} territory={territory} title={type} /> )} /> </> ) } export default TypeaheadWithPillsAsyncCustomOptions
import React from 'react' import { Typeahead } from 'playbook-ui' const synths = [ { label: 'Oberheim', value: 'OBXa' }, { label: 'Moog', value: 'Minimoog' }, { label: 'Roland', value: 'Juno' }, { label: 'Korg', value: 'MS-20' }, ] const cities = [ { label: 'Budapest', value: 'Hungary' }, { label: 'Singapore', value: 'Singapore' }, { label: 'Oslo', value: 'Norway' }, { label: 'Lagos', value: 'Nigeria' }, ] const TypeaheadInline = (props) => { return ( <> <Typeahead inline isMulti label="Synths" options={synths} /> <Typeahead inline isMulti label="Placeholder Plus Icon" options={cities} placeholder="Add cities" plusIcon /> </> ) } export default TypeaheadInline
import React from 'react' import { Typeahead } from 'playbook-ui' const labels = [ { label: 'Verve', value: '1956' }, { label: 'Stax', value: '1957' }, { label: 'Motown', value: '1959' }, { label: 'Kudu', value: '1971' }, { label: 'Stones Throw', value: '1996' }, ] const expressionists = [ { label: 'Kandinsky', value: 'Russia' }, { label: 'Klee', value: 'Switzerland' }, { label: 'Kokoschka', value: 'Austria' }, { label: 'Kirchner', value: 'Germany' }, ] const TypeaheadMultiKit = (props) => { return ( <> <Typeahead defaultValue={[labels[0]]} isMulti label="Badges" multiKit="badge" options={labels} /> <Typeahead defaultValue={[expressionists[0]]} isMulti label="Small Pills" multiKit="smallPill" options={expressionists} /> </> ) } export default TypeaheadMultiKit
import React from 'react' import { Typeahead } from 'playbook-ui' const options = [ { label: 'Jardim', value: 'Portuguese' }, { label: 'Garten', value: 'German' }, { label: 'Giardino', value: 'Italian' }, { label: 'Jardín', value: 'Spanish' }, ] const TypeaheadCreateable = (props) => { return ( <Typeahead createable isMulti label="User Created Options" options={options} /> ) } export default TypeaheadCreateable
import React from 'react' import { Typeahead } from 'playbook-ui' /** * * @const filterResults * @ignore * @returns {[Object]} - a custom mapping of objects, minimally containing * `value` and `label` among other possible fields * @summary - for doc example purposes only */ const filterResults = (results) => results.items.map((result) => { return { label: result.login, value: result.id, } }) /** * * @const promiseOptions * @ignore * @returns {Promise} - fetch API data results from Typeahead input text * @see - https://react-select.com/home#async * @summary - for doc example purposes only */ const promiseOptions = (inputValue) => new Promise((resolve) => { if (inputValue) { fetch(`https://api.github.com/search/users?q=${inputValue}`) .then((response) => response.json()) .then((results) => { resolve(results.items ? filterResults(results) : []) }) } else { resolve([]) } }) const TypeaheadAsyncCreateable = (props) => { return ( <Typeahead async createable isMulti label="Existing or User Created Options" loadOptions={promiseOptions} /> ) } export default TypeaheadAsyncCreateable
Typeahead w/ Error shows that an option must be selected or the selected option is invalid (i.e., when used in a form it signals a user to fix an error).
import React, { useState, useEffect } from 'react' import { Typeahead, Icon } from 'playbook-ui' const options = [ { label: 'Orange', value: '#FFA500' }, { label: 'Red', value: '#FF0000' }, { label: 'Green', value: '#00FF00' }, { label: 'Blue', value: '#0000FF' }, ] const TypeaheadErrorState = (props) => { const error = (<> <Icon icon="warning" /> Please make a valid selection </>) const [errorState, setErrorState] = useState(error); const [searchValue, setSearchValue] = useState(null); const handleOnChange = (value) => setSearchValue(value) useEffect(() => { if(searchValue) { setErrorState("") } else { setErrorState(error) } }, [searchValue]) return ( <Typeahead error={errorState} label="Colors" onChange={handleOnChange} options={options} /> ) } export default TypeaheadErrorState
/* eslint-disable react/no-multi-comp */ import React, { useState } from 'react' import { Button, Typeahead } from 'playbook-ui' const options = [ { label: 'Orange', value: '#FFA500' }, { label: 'Red', value: '#FF0000' }, { label: 'Green', value: '#00FF00' }, { label: 'Blue', value: '#0000FF' }, { label: 'Amaranth', value: '#9F2B68' }, { label: 'Key Lime', value: '#DAF7A6' }, { label: 'Turquois', value: '#00FFD0' }, ] const TypeaheadCustomMenuList = (props) => { const defaultColorOptions = options.slice(0, 3) const [colorOptions, setColorOptions] = useState(defaultColorOptions) const moreToLoad = colorOptions.length == defaultColorOptions.length const loadColors = moreToLoad ? () => setColorOptions(options) : () => setColorOptions(defaultColorOptions) const menuListProps = { footer: (<Button margin="sm" onClick={loadColors} text={`Load ${moreToLoad ? "More" : "Less"}`} />) } const MenuList = (props) => ( <Typeahead.MenuList {...menuListProps} /> ) return ( <Typeahead components={{ MenuList }} label="Colors" options={colorOptions} /> ) } export default TypeaheadCustomMenuList
import React from 'react' import { Typeahead } from 'playbook-ui' const options = [ { label: 'Orange', value: '#FFA500' }, { label: 'Red', value: '#FF0000' }, { label: 'Green', value: '#00FF00' }, { label: 'Blue', value: '#0000FF' }, ] const TypeaheadMarginBottom = (props) => { return ( <> <Typeahead label="None" marginBottom="none" options={options} /> <Typeahead label="XXS" marginBottom="xxs" options={options} /> <Typeahead label="XS" marginBottom="xs" options={options} /> <Typeahead label="Default - SM" options={options} /> <Typeahead label="MD" marginBottom="md" options={options} /> <Typeahead label="LG" marginBottom="lg" options={options} /> <Typeahead label="XL" marginBottom="xl" options={options} /> </> ) } export default TypeaheadMarginBottom
Change the form pill color by passing the optional pillColor
prop. Product, Data, and Status colors are available options. Check them out here in the Form Pill colors example.
import React from 'react' import { Typeahead } from 'playbook-ui' const options = [ { label: 'Windows', value: '#FFA500' }, { label: 'Siding', value: '#FF0000' }, { label: 'Doors', value: '#00FF00' }, { label: 'Roofs', value: '#0000FF' }, ] const TypeaheadWithPills = (props) => { return ( <> <Typeahead isMulti label="Colors" options={options} pillColor="neutral" placeholder="" /> </> ) } export default TypeaheadWithPills
For Form Pills with longer text, the truncate
global prop can be used to truncate the label within each Form Pill. Hover over the truncated Form Pill and a Tooltip containing the text or tag section of the Form Pill will appear. See here for more information on the truncate global prop.
import React from 'react' import { Typeahead } from 'playbook-ui' const names = [ { label: 'Alexander Nathaniel Montgomery', value: 'Alexander Nathaniel Montgomery' }, { label: 'Isabella Anastasia Wellington', value: 'Isabella Anastasia Wellington' }, { label: 'Christopher Maximilian Harrington', value: 'Christopher Maximilian Harrington' }, { label: 'Elizabeth Seraphina Kensington', value: 'Elizabeth Seraphina Kensington' }, { label: 'Theodore Jonathan Abernathy', value: 'Theodore Jonathan Abernathy' }, ] const TypeaheadTruncatedText = (props) => { return ( <> <Typeahead htmlOptions={{ style: { maxWidth: "240px" }}} isMulti label="Truncation Within Typeahead" options={names} truncate={1} /> </> ) } export default TypeaheadTruncatedText
import React from 'react' import { Typeahead } from 'playbook-ui' const options = [ { label: 'Orange', value: '#FFA500' }, { label: 'Red', value: '#FF0000' }, { label: 'Green', value: '#00FF00' }, { label: 'Blue', value: '#0000FF' }, ] const TypeaheadDisabled = (props) => { return ( <Typeahead disabled label="Colors" options={options} /> ) } export default TypeaheadDisabled
By default, text is not preserved in the typeahead kit when you click off of the input field. You can utilize the preserveSearchInput
prop in order to prevent text from being cleared when the field loses focus
import React from 'react' import { Typeahead } from 'playbook-ui' const options = [ { label: 'Orange', value: '#FFA500' }, { label: 'Red', value: '#FF0000' }, { label: 'Green', value: '#00FF00' }, { label: 'Blue', value: '#0000FF' }, ] const TypeaheadPreserveInput = (props) => { return ( <Typeahead label="Colors" options={options} preserveSearchInput /> ) } export default TypeaheadPreserveInput