diff --git a/src/utils/hooks/useCharacterCount/Counter.tsx b/src/utils/hooks/useCharacterCount/Counter.tsx new file mode 100644 index 00000000..a2a1da6e --- /dev/null +++ b/src/utils/hooks/useCharacterCount/Counter.tsx @@ -0,0 +1,28 @@ +import styled from 'styled-components'; + +import { red, yellow } from '../../../color'; +import { Paragraph } from '../../../typography'; + +interface MessageType { + warning: boolean; + error: boolean; +} + +interface Attrs { + theme: { name: string }; +} + +function attrs({ theme }: Attrs) { + return { + size: 3, + format: theme.name === 'dark' ? 'soft' : 'alternative', + }; +} + +export const Counter = styled(Paragraph).attrs(attrs)` + margin-top: 0.25rem; + margin-bottom: 0; + + ${(p) => p.warning && { fontWeight: 800, color: yellow(600) }}; + ${(p) => p.error && { fontWeight: 600, color: red(500) }}; +`; diff --git a/src/utils/hooks/useCharacterCount/useCharacterCount.state.ts b/src/utils/hooks/useCharacterCount/useCharacterCount.state.ts new file mode 100644 index 00000000..a9901052 --- /dev/null +++ b/src/utils/hooks/useCharacterCount/useCharacterCount.state.ts @@ -0,0 +1,42 @@ +export interface CharacterCountState { + error?: boolean; + warning?: boolean; + remainingCharacters?: number; +} + +export type UserAction = + | { type: 'SET_ERROR'; payload?: undefined } + | { type: 'SET_WARNING'; payload?: undefined } + | { type: 'RESET_STATUS'; payload?: undefined } + | { type: 'SET_REMAINING_CHARACTERS'; payload: number }; + +export function reducer( + state: CharacterCountState, + { type, payload }: UserAction +): CharacterCountState { + switch (type) { + case 'SET_ERROR': + return { + ...state, + error: true, + warning: false, + }; + case 'SET_WARNING': + return { + ...state, + warning: true, + error: false, + }; + case 'RESET_STATUS': + return { + ...state, + warning: false, + error: false, + }; + case 'SET_REMAINING_CHARACTERS': + return { + ...state, + remainingCharacters: payload, + }; + } +} diff --git a/src/utils/hooks/useCharacterCount/useCharacterCount.ts b/src/utils/hooks/useCharacterCount/useCharacterCount.ts new file mode 100644 index 00000000..1929999a --- /dev/null +++ b/src/utils/hooks/useCharacterCount/useCharacterCount.ts @@ -0,0 +1,77 @@ +import { useReducer, useCallback } from 'react'; + +import { + reducer, + CharacterCountState, + UserAction, +} from './useCharacterCount.state'; + +export interface UseCharacterCountInit { + value?: string; + /** Max number of characters allowed */ + maxCharacters?: number; + /** Shows warning color when threshold is met */ + warningThreshold?: number; +} + +export interface UseCharacterCount { + state: CharacterCountState; + dispatch: React.Dispatch; + handleChange: (value: string) => void; + clean: () => void; +} + +/** + * Based on withCharacterCount, this provides handlers for a character count message to create a controlled component. + * Helpful to use in conjuction with custom inputs that need more control of input messages + */ + +export function useCharacterCount({ + maxCharacters = 10, + warningThreshold = 5, +}: UseCharacterCountInit): UseCharacterCount { + const [state, dispatch] = useReducer(reducer, { + remainingCharacters: maxCharacters, + }); + + function handleError() { + dispatch({ + type: 'SET_ERROR', + }); + } + + function handleWarn() { + dispatch({ + type: 'SET_WARNING', + }); + } + + const clean = useCallback(() => { + dispatch({ + type: 'RESET_STATUS', + }); + }, []); + + const handleChange = useCallback( + (value: string) => { + const remaining = maxCharacters - value.length; + if (remaining <= warningThreshold && remaining > 0) + handleWarn(); + else if (remaining <= 0) handleError(); + else clean(); + + dispatch({ + type: 'SET_REMAINING_CHARACTERS', + payload: remaining, + }); + }, + [clean, maxCharacters, warningThreshold] + ); + + return { + state, + dispatch, + handleChange, + clean, + }; +} diff --git a/src/utils/index.ts b/src/utils/index.ts index e4cc7257..abac2d31 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -28,6 +28,8 @@ export { usePortal_DEPRECATED, validate, ANCHOR_POINTS } from './hooks/usePortal export { useStateTransmorphic } from './hooks/useStateTransmorphic'; export { useStyleVars } from './hooks/useStyleVars'; export { withIris } from './HOCs/withIris'; +export { useCharacterCount } from './hooks/useCharacterCount/useCharacterCount'; +export { Counter } from './hooks/useCharacterCount/Counter'; export type { Attach, AttachAlias, SimpleAnimation } from './hooks/usePortal_DEPRECATED'; export type { onClose } from './events/onClose';