Skip to content

Commit

Permalink
Add support for selectors/pseudo classes (#119)
Browse files Browse the repository at this point in the history
Co-authored-by: Matt Shwery <[email protected]>
  • Loading branch information
brandongregoryscott and mshwery authored Nov 4, 2022
1 parent bd33f65 commit 4b96abf
Show file tree
Hide file tree
Showing 35 changed files with 343 additions and 229 deletions.
50 changes: 29 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,18 @@ npm install --save ui-box
## Usage

```jsx
import Box from "ui-box";
import Box from 'ui-box'

function Button(props) {
return <Box is="button" padding="10px" background="red" {...props} />;
return <Box is="button" padding="10px" background="red" {...props} />
}

function Example() {
return (
<Button disabled margin="10px">
Hi
</Button>
);
)
}
```

Expand Down Expand Up @@ -74,6 +74,18 @@ Type: `string`

The className prop you know and love. Internally it gets enhanced with additional class names for the CSS properties you specify.

##### selectors

Type: `object<selector: string, props: CssProps>`

This prop allows you to define selectors and custom styles to apply when the selector condition is met. This can be used to create richer element interactions, such as hover or focus states, without the use of another css-in-js library.

```tsx
<Box is="a" selectors={{ '&:hover': { cursor: 'pointer', textDecoration: 'underline' } }}>
Hello world
</Box>
```

##### CSS properties

All of these CSS properties are supported. You can pass either a string or a number (which gets converted to a `px` value). The shorthand properties with repeated values only accept a single value, e.g. `margin="10px"` works but `margin="10px 20px"` does not. You can use the x/y props (e.g. `marginX`/`marginY`) to achieve the same thing.
Expand Down Expand Up @@ -299,19 +311,19 @@ These enhancer groups are also exported. They're all objects with `{ propTypes,
By default `ui-box` uses `ub-` as the classname prefix before all ui-box generated classnames. You can alter this by using `setClassNamePrefix('whatever-you-want-')`. Note that the delimiter is included in the prefix... this is to support backwards compatibility with the old classnames (< v3), which you can achieve using something like this:

```js
import { setClassNamePrefix } from "ui-box";
setClassNamePrefix("📦");
import { setClassNamePrefix } from 'ui-box'
setClassNamePrefix('📦')
```

### Safe `href`s

By default `ui-box` ensures that urls use safe protocols when passed to an element. We built this functionality into `ui-box` to protect the end users of the products you are building. You can opt-out of this by using `configureSafeHref({enabled?: boolean, origin?: string})`. This allows you to configure which protocols are acceptable (`http:`, `https:`, `mailto:`, `tel:`, and `data:`) and that the correct `rel` values are added (`noopener`, `noreferrer`(for external links)).

```js
import { configureSafeHref } from "ui-box";
import { configureSafeHref } from 'ui-box'
configureSafeHref({
enabled: true, // the default behavior
});
enabled: true // the default behavior
})
```

```js
Expand All @@ -337,19 +349,15 @@ To render the styles on the server side just use [`ReactDOMServer.renderToString
For example:

```js
"use strict";
const React = require("react");
const ReactDOMServer = require("react-dom/server");
const { default: Box, extractStyles } = require(".");
'use strict'
const React = require('react')
const ReactDOMServer = require('react-dom/server')
const { default: Box, extractStyles } = require('.')

const element = React.createElement(
Box,
{ margin: "10px", color: "red" },
"hi"
);
const element = React.createElement(Box, { margin: '10px', color: 'red' }, 'hi')

const html = ReactDOMServer.renderToString(element);
const { styles, cache } = extractStyles();
const html = ReactDOMServer.renderToString(element)
const { styles, cache } = extractStyles()

const page = `
<!DOCTYPE html>
Expand All @@ -370,8 +378,8 @@ const page = `
</script>
</body>
</html>
`;
console.log(page);
`
console.log(page)
```

## Development
Expand Down
7 changes: 7 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,13 @@
"webpack": "^4.30.0",
"xo": "^0.24.0"
},
"prettier": {
"semi": false,
"singleQuote": true,
"printWidth": 120,
"tabWidth": 2,
"useTabs": false
},
"xo": {
"parser": "@typescript-eslint/parser",
"extends": [
Expand Down
4 changes: 2 additions & 2 deletions src/box.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import React, { forwardRef } from 'react'
import React from 'react'
import PropTypes from 'prop-types'
import { BoxProps } from './types/box-types'
import { propTypes } from './enhancers'
import enhanceProps from './enhance-props'
import { extractAnchorProps, getUseSafeHref } from './utils/safeHref'

const Box = forwardRef(<E extends React.ElementType>({ is, children, allowUnsafeHref, ...props }: BoxProps<E>, ref: React.Ref<Element>) => {
const Box = React.forwardRef(<E extends React.ElementType>({ is, children, allowUnsafeHref, ...props }: BoxProps<E>, ref: React.Ref<Element>) => {
// Convert the CSS props to class names (and inject the styles)
const {className, enhancedProps: parsedProps} = enhanceProps(props)

Expand Down
8 changes: 4 additions & 4 deletions src/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import {BoxPropValue} from './types/enhancers'
type CacheValue = BoxPropValue
let cache = new Map<string, string>()

export function get(property: string, value: CacheValue) {
return cache.get(property + value)
export function get(property: string, value: CacheValue, selectorHead = '') {
return cache.get(selectorHead + property + value)
}

export function set(property: string, value: CacheValue | object, className: string) {
export function set(property: string, value: CacheValue | object, className: string, selectorHead = '') {
if (process.env.NODE_ENV !== 'production') {
const valueType = typeof value
if (
Expand All @@ -22,7 +22,7 @@ export function set(property: string, value: CacheValue | object, className: str
}
}

cache.set(property + value, className)
cache.set(selectorHead + property + value, className)
}

export function entries() {
Expand Down
55 changes: 34 additions & 21 deletions src/enhance-props.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import {propEnhancers} from './enhancers'
import { propEnhancers } from './enhancers'
import expandAliases from './expand-aliases'
import * as cache from './cache'
import * as styles from './styles'
import {Without} from './types/box-types'
import {EnhancerProps} from './types/enhancers'
import { Without } from './types/box-types'
import { EnhancerProps } from './types/enhancers'

type PreservedProps = Without<React.ComponentProps<any>, keyof EnhancerProps>

Expand All @@ -12,45 +12,58 @@ interface EnhancedPropsResult {
enhancedProps: PreservedProps
}

function noAnd(s: string): string {
return s.replace(/&/g, '')
}

/**
* Converts the CSS props to class names and inserts the styles.
*/
export default function enhanceProps(rawProps: EnhancerProps & React.ComponentPropsWithoutRef<any>): EnhancedPropsResult {
const propsMap = expandAliases(rawProps)
export default function enhanceProps(
props: EnhancerProps & React.ComponentPropsWithoutRef<any>,
selectorHead = ''
): EnhancedPropsResult {
const propsMap = expandAliases(props)
const preservedProps: PreservedProps = {}
let className = rawProps.className || ''
let className: string = props.className || ''

for (const [propName, propValue] of propsMap) {
const cachedClassName = cache.get(propName, propValue)
if (cachedClassName) {
className = `${className} ${cachedClassName}`
for (const [property, value] of propsMap) {
if (value && typeof value === 'object') {
const prop = property === 'selectors' ? '' : property
const parsed = enhanceProps(value, noAnd(selectorHead + prop))
className = `${className} ${parsed.className}`
continue
}

const enhancer = propEnhancers[property]
if (!enhancer) {
// Pass through native props. e.g: disabled, value, type
preservedProps[property] = value
continue
}

const enhancer = propEnhancers[propName]
// Skip false boolean enhancers. e.g: `clearfix={false}`
// Also allows omitting props via overriding with `null` (i.e: neutralising props)
if (
enhancer &&
(propValue === null || propValue === undefined || propValue === false)
) {
if (value === null || value === undefined || value === false) {
continue
} else if (!enhancer) {
// Pass through native props. e.g: disabled, value, type
preservedProps[propName] = propValue
}

const cachedClassName = cache.get(property, value, selectorHead)
if (cachedClassName) {
className = `${className} ${cachedClassName}`
continue
}

const newCss = enhancer(propValue)
const newCss = enhancer(value, selectorHead)
// Allow enhancers to return null for invalid values
if (newCss) {
styles.add(newCss.styles)
cache.set(propName, propValue, newCss.className)
cache.set(property, value, newCss.className, selectorHead)
className = `${className} ${newCss.className}`
}
}

className = className.trim()

return {className, enhancedProps: preservedProps}
return { className, enhancedProps: preservedProps }
}
18 changes: 9 additions & 9 deletions src/enhancers/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,13 @@ const backgroundBlendMode = {
}

export const propEnhancers: PropEnhancers = {
background: (value: PropEnhancerValueType) => getCss(background, value),
backgroundBlendMode: (value: PropEnhancerValueType) => getCss(backgroundBlendMode, value),
backgroundClip: (value: PropEnhancerValueType) => getCss(backgroundClip, value),
backgroundColor: (value: PropEnhancerValueType) => getCss(backgroundColor, value),
backgroundImage: (value: PropEnhancerValueType) => getCss(backgroundImage, value),
backgroundOrigin: (value: PropEnhancerValueType) => getCss(backgroundOrigin, value),
backgroundPosition: (value: PropEnhancerValueType) => getCss(backgroundPosition, value),
backgroundRepeat: (value: PropEnhancerValueType) => getCss(backgroundRepeat, value),
backgroundSize: (value: PropEnhancerValueType) => getCss(backgroundSize, value)
background: (value: PropEnhancerValueType, selector: string) => getCss(background, value, selector),
backgroundBlendMode: (value: PropEnhancerValueType, selector: string) => getCss(backgroundBlendMode, value, selector),
backgroundClip: (value: PropEnhancerValueType, selector: string) => getCss(backgroundClip, value, selector),
backgroundColor: (value: PropEnhancerValueType, selector: string) => getCss(backgroundColor, value, selector),
backgroundImage: (value: PropEnhancerValueType, selector: string) => getCss(backgroundImage, value, selector),
backgroundOrigin: (value: PropEnhancerValueType, selector: string) => getCss(backgroundOrigin, value, selector),
backgroundPosition: (value: PropEnhancerValueType, selector: string) => getCss(backgroundPosition, value, selector),
backgroundRepeat: (value: PropEnhancerValueType, selector: string) => getCss(backgroundRepeat, value, selector),
backgroundSize: (value: PropEnhancerValueType, selector: string) => getCss(backgroundSize, value, selector)
}
8 changes: 4 additions & 4 deletions src/enhancers/border-radius.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ const borderBottomRightRadius = {
}

export const propEnhancers: PropEnhancers = {
borderBottomLeftRadius: (value: PropEnhancerValueType) => getCss(borderBottomLeftRadius, value),
borderBottomRightRadius: (value: PropEnhancerValueType) => getCss(borderBottomRightRadius, value),
borderTopLeftRadius: (value: PropEnhancerValueType) => getCss(borderTopLeftRadius, value),
borderTopRightRadius: (value: PropEnhancerValueType) => getCss(borderTopRightRadius, value)
borderBottomLeftRadius: (value: PropEnhancerValueType, selector: string) => getCss(borderBottomLeftRadius, value, selector),
borderBottomRightRadius: (value: PropEnhancerValueType, selector: string) => getCss(borderBottomRightRadius, value, selector),
borderTopLeftRadius: (value: PropEnhancerValueType, selector: string) => getCss(borderTopLeftRadius, value, selector),
borderTopRightRadius: (value: PropEnhancerValueType, selector: string) => getCss(borderTopRightRadius, value, selector)
}
34 changes: 17 additions & 17 deletions src/enhancers/borders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export const propAliases = {
]
}

export const propValidators: PropValidators = { }
export const propValidators: PropValidators = {}

if (process.env.NODE_ENV !== 'production') {
propValidators.borderColor = value => {
Expand Down Expand Up @@ -160,20 +160,20 @@ const borderBottomWidth = {
}

export const propEnhancers: PropEnhancers = {
borderBottom: (value: PropEnhancerValueType) => getCss(borderBottom, value),
borderBottomColor: (value: PropEnhancerValueType) => getCss(borderBottomColor, value),
borderBottomStyle: (value: PropEnhancerValueType) => getCss(borderBottomStyle, value),
borderBottomWidth: (value: PropEnhancerValueType) => getCss(borderBottomWidth, value),
borderLeft: (value: PropEnhancerValueType) => getCss(borderLeft, value),
borderLeftColor: (value: PropEnhancerValueType) => getCss(borderLeftColor, value),
borderLeftStyle: (value: PropEnhancerValueType) => getCss(borderLeftStyle, value),
borderLeftWidth: (value: PropEnhancerValueType) => getCss(borderLeftWidth, value),
borderRight: (value: PropEnhancerValueType) => getCss(borderRight, value),
borderRightColor: (value: PropEnhancerValueType) => getCss(borderRightColor, value),
borderRightStyle: (value: PropEnhancerValueType) => getCss(borderRightStyle, value),
borderRightWidth: (value: PropEnhancerValueType) => getCss(borderRightWidth, value),
borderTop: (value: PropEnhancerValueType) => getCss(borderTop, value),
borderTopColor: (value: PropEnhancerValueType) => getCss(borderTopColor, value),
borderTopStyle: (value: PropEnhancerValueType) => getCss(borderTopStyle, value),
borderTopWidth: (value: PropEnhancerValueType) => getCss(borderTopWidth, value)
borderBottom: (value: PropEnhancerValueType, selector: string) => getCss(borderBottom, value, selector),
borderBottomColor: (value: PropEnhancerValueType, selector: string) => getCss(borderBottomColor, value, selector),
borderBottomStyle: (value: PropEnhancerValueType, selector: string) => getCss(borderBottomStyle, value, selector),
borderBottomWidth: (value: PropEnhancerValueType, selector: string) => getCss(borderBottomWidth, value, selector),
borderLeft: (value: PropEnhancerValueType, selector: string) => getCss(borderLeft, value, selector),
borderLeftColor: (value: PropEnhancerValueType, selector: string) => getCss(borderLeftColor, value, selector),
borderLeftStyle: (value: PropEnhancerValueType, selector: string) => getCss(borderLeftStyle, value, selector),
borderLeftWidth: (value: PropEnhancerValueType, selector: string) => getCss(borderLeftWidth, value, selector),
borderRight: (value: PropEnhancerValueType, selector: string) => getCss(borderRight, value, selector),
borderRightColor: (value: PropEnhancerValueType, selector: string) => getCss(borderRightColor, value, selector),
borderRightStyle: (value: PropEnhancerValueType, selector: string) => getCss(borderRightStyle, value, selector),
borderRightWidth: (value: PropEnhancerValueType, selector: string) => getCss(borderRightWidth, value, selector),
borderTop: (value: PropEnhancerValueType, selector: string) => getCss(borderTop, value, selector),
borderTopColor: (value: PropEnhancerValueType, selector: string) => getCss(borderTopColor, value, selector),
borderTopStyle: (value: PropEnhancerValueType, selector: string) => getCss(borderTopStyle, value, selector),
borderTopWidth: (value: PropEnhancerValueType, selector: string) => getCss(borderTopWidth, value, selector)
}
2 changes: 1 addition & 1 deletion src/enhancers/box-shadow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,5 @@ const boxShadow = {
}

export const propEnhancers: PropEnhancers = {
boxShadow: (value: PropEnhancerValueType) => getCss(boxShadow, value)
boxShadow: (value: PropEnhancerValueType, selector: string) => getCss(boxShadow, value, selector)
}
12 changes: 6 additions & 6 deletions src/enhancers/dimensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,10 @@ const maxHeight = {
}

export const propEnhancers: PropEnhancers = {
height: (value: PropEnhancerValueType) => getCss(height, value),
maxHeight: (value: PropEnhancerValueType) => getCss(maxHeight, value),
maxWidth: (value: PropEnhancerValueType) => getCss(maxWidth, value),
minHeight: (value: PropEnhancerValueType) => getCss(minHeight, value),
minWidth: (value: PropEnhancerValueType) => getCss(minWidth, value),
width: (value: PropEnhancerValueType) => getCss(width, value)
height: (value: PropEnhancerValueType, selector: string) => getCss(height, value, selector),
maxHeight: (value: PropEnhancerValueType, selector: string) => getCss(maxHeight, value, selector),
maxWidth: (value: PropEnhancerValueType, selector: string) => getCss(maxWidth, value, selector),
minHeight: (value: PropEnhancerValueType, selector: string) => getCss(minHeight, value, selector),
minWidth: (value: PropEnhancerValueType, selector: string) => getCss(minWidth, value, selector),
width: (value: PropEnhancerValueType, selector: string) => getCss(width, value, selector)
}
34 changes: 17 additions & 17 deletions src/enhancers/flex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,21 +134,21 @@ const placeSelf = {
}

export const propEnhancers: PropEnhancers = {
alignContent: (value: PropEnhancerValueType) => getCss(alignContent, value),
alignItems: (value: PropEnhancerValueType) => getCss(alignItems, value),
alignSelf: (value: PropEnhancerValueType) => getCss(alignSelf, value),
flex: (value: PropEnhancerValueType) => getCss(flex, value),
flexBasis: (value: PropEnhancerValueType) => getCss(flexBasis, value),
flexDirection: (value: PropEnhancerValueType) => getCss(flexDirection, value),
flexFlow: (value: PropEnhancerValueType) => getCss(flexFlow, value),
flexGrow: (value: PropEnhancerValueType) => getCss(flexGrow, value),
flexShrink: (value: PropEnhancerValueType) => getCss(flexShrink, value),
flexWrap: (value: PropEnhancerValueType) => getCss(flexWrap, value),
justifyContent: (value: PropEnhancerValueType) => getCss(justifyContent, value),
justifyItems: (value: PropEnhancerValueType) => getCss(justifyItems, value),
justifySelf: (value: PropEnhancerValueType) => getCss(justifySelf, value),
order: (value: PropEnhancerValueType) => getCss(order, value),
placeContent: (value: PropEnhancerValueType) => getCss(placeContent, value),
placeItems: (value: PropEnhancerValueType) => getCss(placeItems, value),
placeSelf: (value: PropEnhancerValueType) => getCss(placeSelf, value)
alignContent: (value: PropEnhancerValueType, selector: string) => getCss(alignContent, value, selector),
alignItems: (value: PropEnhancerValueType, selector: string) => getCss(alignItems, value, selector),
alignSelf: (value: PropEnhancerValueType, selector: string) => getCss(alignSelf, value, selector),
flex: (value: PropEnhancerValueType, selector: string) => getCss(flex, value, selector),
flexBasis: (value: PropEnhancerValueType, selector: string) => getCss(flexBasis, value, selector),
flexDirection: (value: PropEnhancerValueType, selector: string) => getCss(flexDirection, value, selector),
flexFlow: (value: PropEnhancerValueType, selector: string) => getCss(flexFlow, value, selector),
flexGrow: (value: PropEnhancerValueType, selector: string) => getCss(flexGrow, value, selector),
flexShrink: (value: PropEnhancerValueType, selector: string) => getCss(flexShrink, value, selector),
flexWrap: (value: PropEnhancerValueType, selector: string) => getCss(flexWrap, value, selector),
justifyContent: (value: PropEnhancerValueType, selector: string) => getCss(justifyContent, value, selector),
justifyItems: (value: PropEnhancerValueType, selector: string) => getCss(justifyItems, value, selector),
justifySelf: (value: PropEnhancerValueType, selector: string) => getCss(justifySelf, value, selector),
order: (value: PropEnhancerValueType, selector: string) => getCss(order, value, selector),
placeContent: (value: PropEnhancerValueType, selector: string) => getCss(placeContent, value, selector),
placeItems: (value: PropEnhancerValueType, selector: string) => getCss(placeItems, value, selector),
placeSelf: (value: PropEnhancerValueType, selector: string) => getCss(placeSelf, value, selector)
}
Loading

0 comments on commit 4b96abf

Please sign in to comment.