@twstyled/core
Version:
twstyled -- the full-featured Tailwind CSS + CSS in JS solution with blazing fast build times and no runtime overhead
105 lines (104 loc) • 4.74 kB
JavaScript
/**
* This file contains an runtime version of `styled` component. Responsibilities of the component are:
* - returns ReactElement based on HTML tag used with `styled` or custom React Component
* - injects classNames for the returned component
* - injects CSS variables used to define dynamic styles based on props
*/
import { createElement, forwardRef } from 'react';
import validAttr from '@emotion/is-prop-valid';
import cx from 'clsx';
// Workaround for rest operator
const restOp = (obj, keysToExclude) => Object.keys(obj)
.filter((prop) => !keysToExclude.includes(prop))
.reduce((acc, curr) => Object.assign(acc, { [curr]: obj[curr] }), {}); // rest operator workaround
const warnIfInvalid = (value, componentName) => {
if (process.env.NODE_ENV !== 'production') {
if (typeof value === 'string' ||
// eslint-disable-next-line no-self-compare
(typeof value === 'number' && isFinite(value))) {
return;
}
const stringified = typeof value === 'object' ? JSON.stringify(value) : String(value);
// eslint-disable-next-line no-console
console.warn(`An interpolation evaluated to '${stringified}' in the component '${componentName}', which is probably a mistake. You should explicitly cast or transform the value to a string.`);
}
};
function styled(tag) {
return (options) => {
if (process.env.NODE_ENV !== 'production') {
if (Array.isArray(options)) {
// We received a strings array since it's used as a tag
throw new Error('Using the "styled" tag in runtime is not supported. Make sure you have set up the Babel plugin correctly. See https://github.com/callstack/linaria#setup');
}
}
const render = (props, ref) => {
const { as: component = tag, class: className } = props;
const rest = restOp(props, ['as', 'class']);
let filteredProps;
// Check if it's an HTML tag and not a custom element
if (typeof component === 'string' && component.indexOf('-') === -1) {
filteredProps = {};
// eslint-disable-next-line guard-for-in
for (const key in rest) {
if (key === 'as' || validAttr(key)) {
// Don't pass through invalid attributes to HTML elements
filteredProps[key] = rest[key];
}
}
}
else {
filteredProps = rest;
}
filteredProps.ref = ref;
const { vars, twvars } = options;
let { class: classnames } = options;
if (vars) {
const style = {};
// eslint-disable-next-line guard-for-in
for (const name in vars) {
const variable = vars[name];
const result = variable[0];
const value = typeof result === 'function' ? result(props) : result;
warnIfInvalid(value, options.name);
if (twvars && twvars.indexOf(name) !== -1) {
classnames += ` ${value}`;
}
else {
const unit = variable[1] || '';
style[`--${name}`] = `${value}${unit}`;
}
}
filteredProps.style = Object.assign(style, filteredProps.style);
}
filteredProps.className = cx(filteredProps.className || className, classnames);
if (tag.__linaria && tag !== component) {
// If the underlying tag is a styled component, forward the `as` prop
// Otherwise the styles from the underlying component will be ignored
filteredProps.as = component;
return createElement(tag, filteredProps);
}
return createElement(component, filteredProps);
};
const Result = forwardRef
? forwardRef(render)
: // React.forwardRef won't available on older React versions and in Preact
// Fallback to a innerRef prop in that case
(props) => {
const rest = restOp(props, ['innerRef']);
return render(rest, props.innerRef);
};
Result.displayName = options.name;
Result.__linaria = {
className: options.class,
extends: tag
};
return Result;
};
}
export default (process.env.NODE_ENV !== 'production'
? new Proxy(styled, {
get(o, prop) {
return o(prop);
}
})
: styled);