Created
September 22, 2020 05:47
-
-
Save unicornware/c23b0b38c59b22c162f3099363db8c63 to your computer and use it in GitHub Desktop.
Custom React Hooks
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
AriaAttributes, | |
CSSProperties, | |
DOMAttributes, | |
RefAttributes | |
} from 'react' | |
import { ContainerProps, IconProps } from './atoms' | |
/** | |
* Aria attributes and event handlers. | |
*/ | |
export type Attributes<E = HTMLElement> = AriaAttributes & | |
DOMAttributes<E> & | |
RefAttributes<E> & { forwardedRef?: RefAttributes<E>['ref'] } | |
/** | |
* Background size options. | |
*/ | |
export type BackgroundSize = 'auto' | 'contain' | 'cover' | |
/* eslint-disable */ | |
/** | |
* Boolean and string values representing `true` or `false`. | |
*/ | |
export type Booleanish = boolean | 'true' | 'false' | |
/** | |
* {@link Button} component variants. | |
*/ | |
export type ButtonVariant = ( | |
| Variant | |
| VariantOutline | |
| 'link' | |
) | |
/** | |
* Color variants. | |
*/ | |
export type Color = ( | |
| 'accent' | |
| 'black' | |
| 'danger' | |
| 'dark' | |
| 'info' | |
| 'light' | |
| 'primary' | |
| 'muted' | |
| 'secondary' | |
| 'success' | |
| 'warning' | |
| 'white' | |
) | |
/* eslint-enable */ | |
/** | |
* Possible {@link Container} component sizes. | |
*/ | |
export type ContainerSize = 'sm' | 'md' | 'lg' | 'xl' | 'xxl' | |
/** | |
* Common content-sectioning component properties. | |
* | |
* @see {@link https://websitesetup.org/html5-periodical-table/} | |
*/ | |
export interface ContentSectionProps<E = HTMLElement> | |
extends TextContentProps<E> { | |
/** | |
* Background color or outline variant. | |
* | |
* @default false | |
*/ | |
variant?: boolean | Variant | VariantOutline | |
} | |
/* eslint-disable */ | |
/** | |
* Font weights. | |
*/ | |
export type FontWeight = ( | |
| 'hairline' | |
| 'thin' | |
| 'light' | |
| 'normal' | |
| 'medium' | |
| 'semibold' | |
| 'bold' | |
| 'extrabold' | |
| 'black' | |
) | |
/* eslint-enable */ | |
/** | |
* Possible {@link Button} component sizes. | |
*/ | |
export type FormControlSize = Pick<Size, 'sm' | 'lg'> | |
/** | |
* Properties common to all components. | |
*/ | |
export interface GlobalProps<E = HTMLElement> extends Attributes<E> { | |
/** | |
* Content to render inside the component. | |
* | |
* If defined, `innerHTML` must be omitted. | |
*/ | |
children?: DOMAttributes<E>['children'] | |
/** | |
* If defined, wrap inner content in a `Container` component. | |
*/ | |
container?: true | ContainerProps<E> | |
/** | |
* A space-separated list of the classes of the element. | |
* | |
* Classes allows CSS and JavaScript to select and access specific elements | |
* via the class selectors or functions like the method | |
* `Document.getElementsByClassName()`. | |
*/ | |
className?: string | |
/** | |
* An enumerated attribute indicating if the element should be editable by | |
* the user. If so, the browser modifies its widget to allow editing. | |
*/ | |
contentEditable?: Booleanish | 'inherit' | |
/** | |
* Add the class "d-flex" or "d-inline-flex". | |
* | |
* @default false | |
*/ | |
flex?: boolean | 'inline' | |
/** | |
* A Boolean attribute indicates that the element is not yet, or is no longer, | |
* relevant. | |
* | |
* For example, it can be used to hide elements of the page that can't be | |
* used until the login process has been completed. The browser won't render | |
* such elements. | |
* | |
* This attribute must not be used to hide content that could legitimately | |
* be shown. | |
*/ | |
hidden?: boolean | |
/** | |
* Icon to render beside the element text. | |
*/ | |
icon?: IconProps | |
/** | |
* Defines a unique identifier (ID) which must be unique in the whole | |
* document. Its purpose is to identify the element when linking (using a | |
* fragment identifier), scripting, or styling (with CSS). | |
*/ | |
id?: string | |
/** | |
* HTML string to render inside the component. | |
* | |
* If defined, `children` must be omitted. | |
*/ | |
innerHTML?: string | |
/* eslint-disable prettier/prettier */ | |
/** | |
* Provides a hint to browsers as to the type of virtual keyboard | |
* configuration to use when editing this element or its contents. | |
* | |
* Used primarily on `<input>` elements, but is usable on any element while | |
* in contenteditable mode. | |
*/ | |
inputMode?: ( | |
'none' | |
| 'text' | |
| 'decimal' | |
| 'numeric' | |
| 'tel' | |
| 'search' | |
| 'email' | |
| 'url' | |
) | |
/* eslint-enable prettier/prettier */ | |
/** | |
* Specify that a standard HTML element should behave like a defined custom | |
* built-in element. | |
* | |
* - https://html.spec.whatwg.org/multipage/custom-elements.html#attr-is | |
*/ | |
is?: string | |
/** | |
* Helps define the language of an element?: the language that non-editable | |
* elements are in, or the language that editable elements should be written | |
* in by the user. | |
* | |
* The attribute contains one “language tag” (made of hyphen-separated | |
* “language subtags”) in the format defined in Tags for Identifying | |
* Languages (BCP47). xml:lang has priority over it. | |
*/ | |
lang?: string | |
/** | |
* An enumerated attribute defines whether the element may be checked for | |
* spelling errors. | |
* | |
* It may have the following values: | |
* | |
* - true, which indicates that the element should be, if possible, checked | |
* for spelling errors; | |
* - false, which indicates that the element should not be checked for | |
* spelling errors. | |
*/ | |
spellCheck?: Booleanish | |
/** | |
* WAI-ARIA role. | |
*/ | |
role?: string | |
/** | |
* If true, skip the logic in the `useIcon` hook. | |
*/ | |
skipUseIcon?: boolean | |
/** | |
* Contains CSS styling declarations to be applied to the element. Note that | |
* it is recommended for styles to be defined in a separate file or files. | |
* | |
* This attribute and the <style> element have mainly the purpose of | |
* allowing for quick styling, for example for testing purposes. | |
*/ | |
style?: CSSProperties | |
/** | |
* An integer attribute indicating if the element can take input focus (is | |
* focusable), if it should participate to sequential keyboard navigation, | |
* and if so, at what position. | |
* | |
* It can take several values: | |
* | |
* - a negative value means that the element should be focusable, but should | |
* not be reachable via sequential keyboard navigation; | |
* - 0 means that the element should be focusable and reachable via | |
* sequential keyboard navigation, but its relative order is defined by | |
* the platform convention; | |
* - a positive value means that the element should be focusable and | |
* reachable via sequential keyboard navigation; the order in which the | |
* elements are focused is the increasing value of the tabindex. If | |
* several elements share the same tabindex, their relative order follows | |
* their relative positions in the document. | |
*/ | |
tabindex?: number | |
/** | |
* Contains a text representing advisory information related to the element | |
* it belongs to. Such information can typically, but not necessarily, be | |
* presented to the user as a tooltip. | |
*/ | |
title?: string | |
/** | |
* An enumerated attribute that is used to specify whether an element's | |
* attribute values and the values of its Text node children are to be | |
* translated when the page is localized, or whether to leave them | |
* unchanged. | |
* | |
* It can have the following values: | |
* | |
* - empty string and yes, which indicates that the element will be | |
* translated. | |
* - no, which indicates that the element will not be translated. | |
*/ | |
translate?: 'no' | 'yes' | |
/** | |
* Background color or outline variant. | |
* | |
* @default false | |
*/ | |
variant?: boolean | Variant | VariantOutline | |
} | |
/* eslint-disable */ | |
/** | |
* {@link GridBox} component order options. | |
* | |
* @see {@link https://react-bootstrap-v5.netlify.app/layout/grid/} | |
*/ | |
export type GridBoxOrder = ( | |
| '1' | |
| '2' | |
| '3' | |
| '4' | |
| '5' | |
| '6' | |
| '7' | |
| '8' | |
| '9' | |
| '10' | |
| '11' | |
| '12' | |
) | |
/** | |
* {@link GridBox} component span options. | |
* | |
* @see {@link https://react-bootstrap-v5.netlify.app/layout/grid/} | |
*/ | |
export type GridBoxSpan = ( | |
| '1' | |
| '2' | |
| '3' | |
| '4' | |
| '5' | |
| '6' | |
| '7' | |
| '8' | |
| '9' | |
| '10' | |
| '11' | |
| '12' | |
) | |
/* eslint-enable */ | |
/** | |
* Ref attributes for HTML elements. | |
*/ | |
export type HTMLElementRefAttributes = RefAttributes<HTMLElement> | |
/** | |
* Global properties are attributes common to all HTML elements; they can be | |
* used on all elements, though they may have no effect on some elements. | |
* | |
* The properties defined are the ones to be used by this application. | |
* | |
* **https://developer.mozilla.org/docs/Web/HTML/Global_attributes** | |
*/ | |
export type HTMLGlobalProps = Omit<GlobalProps, 'flex' | 'icon' | 'variant'> | |
/** | |
* Pulls the common properties from both types, but only maps the values from | |
* the first argument. | |
* | |
* @example NativeProps<JSX.IntrinsicElements['button'], ButtonProps> | |
* | |
* @see | |
* {@link https://stackoverflow.com/questions/47375916/typescript-how-to-create-type-with-common-properties-of-two-types} | |
*/ | |
export type NativeProps<HTMLElementProps, Props> = { | |
[HTMLProp in keyof HTMLElementProps & keyof Props]: typeof A[HTMLProp] | |
} | |
/** | |
* Common `Form` (button, input, select) element props. | |
*/ | |
export interface PropsForFormElement<E = HTMLElement> extends GlobalProps<E> { | |
/** | |
* Specifies that a form control should have input focus when the page | |
* loads. | |
* | |
* Only one form-associated element in a document can have this attribute | |
* specified. | |
*/ | |
autoFocus?: boolean | |
/** | |
* Indicates that the user cannot interact with the control. | |
* | |
* If this attribute is not specified, the control inherits its setting from | |
* the containing element, for example `<fieldset>`; if there is no containing | |
* element when the `disabled` attribute is set, the control is enabled. | |
*/ | |
disabled?: boolean | |
/** | |
* The `id` of the `<form>` element that the element is associated with. | |
* | |
* If this attribute is not specified, the element must be a descendant of a | |
* form element. | |
* | |
* This attribute enables you to place elements anywhere within a document, | |
* not just as descendants of form elements. | |
*/ | |
form?: string | |
/** | |
* The name of the control. | |
*/ | |
name?: string | |
/** | |
* Current value of the form control. | |
* | |
* Submitted with the form as part of a name/value pair. | |
*/ | |
value?: string | ReadonlyArray<string> | number | |
} | |
/** | |
* Component properties for HTML elements that do not accept inner content. | |
*/ | |
export type PropsForVoidElementTag<E = HTMLElement> = Omit< | |
GlobalProps<E>, | |
'children' | 'dangerouslySetInnerHTML' | |
> | |
/** | |
* Text sizes. | |
*/ | |
export type Size = | |
| 'xs' | |
| 'sm' | |
| 'md' | |
| 'lg' | |
| 'xl' | |
| '2xl' | |
| '3xl' | |
| '4xl' | |
| '5xl' | |
| '6xl' | |
| '7xl' | |
| '8xl' | |
/** | |
* Keys of `scss` `$spacers` map. | |
*/ | |
export type SpacerKey = | |
| 0 | |
| 1 | |
| 2 | |
| 3 | |
| 4 | |
| 5 | |
| 6 | |
| 8 | |
| 9 | |
| 10 | |
| 12 | |
| 14 | |
| 16 | |
| 18 | |
| 20 | |
| 24 | |
| 48 | |
| 56 | |
| 64 | |
/** | |
* Common text content component properties. | |
* | |
* @see {@link https://websitesetup.org/html5-periodical-table/} | |
*/ | |
export interface TextContentProps<E = HTMLElement> extends GlobalProps<E> { | |
/** | |
* Text content color. | |
* | |
* @default false | |
*/ | |
color?: Color | boolean | |
/** | |
* Text content size. | |
* | |
* @default false | |
*/ | |
size?: Size | boolean | |
} | |
/** | |
* Color variants. | |
*/ | |
export type Variant = | |
| 'accent' | |
| 'black' | |
| 'danger' | |
| 'dark' | |
| 'ghost' | |
| 'info' | |
| 'light' | |
| 'primary' | |
| 'secondary' | |
| 'success' | |
| 'warning' | |
| 'white' | |
/** | |
* Outline variants. | |
*/ | |
export type VariantOutline = | |
| 'outline-danger' | |
| 'outline-dark' | |
| 'outline-info' | |
| 'outline-light' | |
| 'outline-primary' | |
| 'outline-secondary' | |
| 'outline-success' | |
| 'outline-warning' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Container, ContainerProps } from '../lib' | |
import { GlobalProps } from '../lib/declarations' | |
import { isBoolean, omit } from 'lodash' | |
import React, { useEffect, useState } from 'react' | |
/** | |
* @file Render props.children in a Container component | |
* @module hooks/useContainer | |
* | |
* @see {@link https://reactjs.org/docs/hooks-reference.html#usestate} | |
* @see {@link https://reactjs.org/docs/hooks-reference.html#useeffect} | |
*/ | |
/** | |
* Renders {@param props.children} in a {@link Container} component. | |
* | |
* @param props - Component properties | |
* @param props.children - Inner content | |
* @param props.container - Boolean or Container component properties | |
*/ | |
export const useContainer = (props: GlobalProps): GlobalProps => { | |
const { children, container: initialContainer = false } = props | |
const containerProps = isBoolean(initialContainer) ? {} : initialContainer | |
const [skip] = useState(!initialContainer) | |
const [mutatedChildren, setMutatedChildren] = useState(children) | |
const [container] = useState(JSON.stringify(containerProps)) | |
useEffect(() => { | |
// Skip hook logic | |
if (skip) return | |
// Parse container state | |
const containerParsed: ContainerProps = JSON.parse(container) | |
// Update state | |
setMutatedChildren(<Container {...containerParsed}>{children}</Container>) | |
}, [children, container, skip]) | |
return { ...omit(props, ['container']), children: mutatedChildren } | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Icon, IconProps } from '../lib' | |
import { GlobalProps } from '../lib/declarations' | |
import classnames from 'classnames' | |
import { cloneDeep, isNull, isUndefined, omit } from 'lodash' | |
import React, { useEffect, useState } from 'react' | |
/** | |
* @file Render icon with props.children | |
* @module hooks/useIcon | |
* | |
* @see {@link https://reactjs.org/docs/hooks-reference.html#usestate} | |
* @see {@link https://reactjs.org/docs/hooks-reference.html#useeffect} | |
*/ | |
/** | |
* Renders an `Icon` component with {@param props.children}. | |
* | |
* If the {@param props.icon.position} isn't defined, the icon will be rendered | |
* on the right. | |
* | |
* @param props - Component properties | |
* @param props.children - Inner content | |
* @param props.icon - Icon component properties | |
* @param props.icon.position - String indication where to position icon | |
*/ | |
export const useIcon = (props: GlobalProps): GlobalProps => { | |
const { | |
children, | |
className: initialClassName = '', | |
icon: initialIcon | |
} = props | |
const [className, setClassName] = useState(initialClassName) | |
const [skip] = useState(!initialIcon) | |
const [dataAttrs, setDataAttrs] = useState<Record<string, any>>({}) | |
const [mutatedChildren, setMutatedChildren] = useState(children) | |
const [icon] = useState(JSON.stringify(initialIcon || '')) | |
useEffect(() => { | |
// Skip hook logic | |
if (skip) return | |
// Parse icon props | |
const iconParsed: IconProps = JSON.parse(icon) | |
// Get icon component | |
const component: JSX.Element = <Icon {...iconParsed} key='icon' /> | |
// Position icon | |
const { position } = iconParsed | |
if (!children) { | |
setMutatedChildren(component) | |
} else if (position === 'left') { | |
setMutatedChildren([component, children]) | |
} else { | |
setMutatedChildren([children, component]) | |
if (['bottom', 'top'].includes(position ?? '')) { | |
setClassName(classes => { | |
return classnames({ | |
[`${classes}`]: true, | |
'd-flex': true, | |
'flex-column': position === 'bottom', | |
'flex-column-reverse': position === 'top' | |
}) | |
}) | |
} | |
} | |
// Set data attributes | |
setDataAttrs({ | |
'data-icon': true, | |
'data-icon-only': isNull(children) || isUndefined(children) | |
}) | |
}, [children, icon, skip]) | |
return { | |
...cloneDeep(omit(props, ['icon'])), | |
...dataAttrs, | |
children: mutatedChildren, | |
className | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { GlobalProps } from '../lib/declarations' | |
import classnames from 'classnames' | |
import { ClassValue } from 'classnames/types' | |
import { isObject, isString, omit, uniq } from 'lodash' | |
import { HTMLAttributes } from 'react' | |
import { useContainer } from './useContainer' | |
import { useIcon } from './useIcon' | |
/** | |
* @file Add global mutations to incoming props | |
* @module hooks/useMutatedProps | |
*/ | |
/** | |
* Adds global mutations to the incoming component properties. | |
* | |
* Mutations (in order, if props are defined): | |
* | |
* - {@param props.icon} will be used to render an {@link Icon} (if defined) | |
* - {@param props.children} will wrapped in a {@link Container} component | |
* - {@param props.innerHTML} will be converted into `dangerouslySetInnerHTML` | |
* - {@param props.flex} (if defined) will be used to update flexbox classes | |
* - {@param props.variant} (if defined) will be used to update the `bg-` class | |
* - {@param inject} will be passed to `classnames` function | |
* - Keys specified in {@param keys} will be removed {@param props} | |
* | |
* @param props - Component properties | |
* @param props.children - Component children | |
* @param props.flex - Append flexbox classes | |
* @param props.icon - Icon component properties | |
* @param props.variant - Append `bg-` classes | |
* @param inject - Classes to inject before {@param props.className} | |
* @param keys - Array of keys to remove from {@param props} | |
*/ | |
export function useMutatedProps< | |
T1 = GlobalProps, | |
Mask = HTMLAttributes<HTMLElement> | |
>(props: T1, injectClass?: ClassValue, keys?: string[]): Mask { | |
const globalProps = props as GlobalProps | |
const withIcon = useIcon(globalProps) | |
const withContainer = useContainer(withIcon) | |
keys = keys || [] | |
if (withContainer.innerHTML) { | |
withContainer.dangerouslySetInnerHTML = { __html: withContainer.innerHTML } | |
keys.push('innerHTML') | |
keys.push('children') | |
} | |
if (withContainer.flex) { | |
const { flex } = withContainer | |
injectClass = isObject(injectClass) ? injectClass : {} | |
injectClass[`d-${isString(flex) ? 'inline-' : ''}flex`] = flex | |
keys.push('flex') | |
} | |
if (withContainer.variant) { | |
const { variant } = withContainer | |
injectClass = isObject(injectClass) ? injectClass : {} | |
injectClass[`${injectClass.btn ? 'btn' : 'bg'}-${variant}`] = variant | |
keys.push('variant') | |
} | |
withContainer.className = classnames(injectClass, withContainer.className) | |
const mutatedProps = { | |
...withContainer, | |
children: globalProps.icon ? withIcon.children : withContainer.children | |
} | |
return omit(mutatedProps, uniq(keys)) as Mask | |
} | |
export default useMutatedProps |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment