Skip to content

Commit

Permalink
feat(Spacing): add calc helper (#1845)
Browse files Browse the repository at this point in the history
  • Loading branch information
tujoworker authored and joakbjerk committed Mar 27, 2023
1 parent 29af060 commit f6ae0a9
Show file tree
Hide file tree
Showing 4 changed files with 247 additions and 119 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,17 @@ const Custom = styled(Space)`
You may use the internals to build helpers suited to your needs.

```tsx
import { sumTypes } from '@dnb/eufemia/components/space/SpacingUtils'

const spacing = (space) => sumTypes(space) + 'rem'
import { calc } from '@dnb/eufemia/components/space/SpacingUtils'

// With Styled Components
const StyledDiv = styled.div`
margin-top: ${spacing('medium large')};
margin-top: ${calc('medium large')};
margin-top: ${calc('medium', 'large')};
margin-top: ${calc('1.5rem', '2rem')};
margin-top: ${calc('24px', '32px')};
`
```

All of the examples do output: `calc(var(--spacing-medium) + var(--spacing-large))`

Invalid values will be corrected to its nearest spacing type (e.g. 17px to `var(--spacing-small)`).
254 changes: 146 additions & 108 deletions packages/dnb-eufemia/src/components/space/SpacingUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,14 @@

import { warn } from '../../shared/component-helper'

import type { SpacingProps } from './types'
import type {
StyleObjectProps,
SpaceType,
SpacingUnknownProps,
SpacingProps,
} from './types'

type Props = SpacingProps | Record<string, unknown>

type StyleObjectProps = {
maxWidth?: string
maxHeight?: string
width?: string
height?: string
}
type SpaceNumber = number

export const spacingDefaultProps = {
space: null,
Expand All @@ -35,27 +33,125 @@ export const spacePatterns = {
'xx-large-x2': 7,
}

export const translateSpace = (type) => {
if (/-x2$/.test(type)) {
return spacePatterns[type.replace(/-x2$/, '')] * 2
/**
* Helper function to generate a calc(var(--spacing-large) + var(--spacing-small))
*
* @param types 'small', '16px', '1rem'
* @returns e.g. calc(var(--spacing-large) + var(--spacing-small))
*/
export const calc = (...types: Array<SpaceType>) => {
const result: Array<string> = []

types.forEach((rawTypes) => {
createTypeModifiers(rawTypes as SpaceType).forEach((type) => {
result.push(`var(--spacing-${type})`)
})
})

return result.length ? `calc(${result.join(' + ')})` : null
}

/**
* Creates a valid space CSS class out from given space types
*
* @param props
* @param Element to check if it should be handled as inline
* @returns "dnb-space__large dnb-space__small"
*/
export const createSpacingClasses = (
props:
| SpacingProps
/**
* To support typical not defined props form components
*/
| SpacingUnknownProps,
Element = null
) => {
const p = Object.isFrozen(props) ? { ...props } : props

if (typeof p.space !== 'undefined') {
if (
typeof p.space === 'string' ||
typeof p.space === 'number' ||
(typeof p.space === 'boolean' && p.space)
) {
p.top = p.right = p.bottom = p.left = p.space
}
if (typeof p.space === 'object') {
for (const i in p.space) {
if (!p[i] && isValidSpaceProp(i)) {
p[i] = p.space[i]
}
}
}
delete p.space
}

return Object.entries(p).reduce((acc, [direction, cur]) => {
if (isValidSpaceProp(direction)) {
if (String(cur) === '0' || String(cur) === 'false') {
acc.push(`dnb-space__${direction}--zero`)
} else if (cur) {
const typeModifiers = createTypeModifiers(cur as SpaceType)

// get the total sum
const sum = sumTypes(typeModifiers)
if (sum > 10) {
warn(
`Spacing of more than 10rem is not supported! You used ${sum} / (${typeModifiers.join(
','
)})`
)
} else {
// auto combine classes
const nearestTypes = findNearestTypes(sum, true)

acc = [
...acc,
...nearestTypes.map(
(type) => `dnb-space__${direction}--${type}`
),
]
}
}
} else if (direction === 'no_collapse') {
acc.push('dnb-space--no-collapse')
if (Element && isInline(Element)) {
acc.push('dnb-space--inline')
}
}

return acc
}, [])
}

// @internal splits types by given string
export const translateSpace = (type: SpaceType) => {
if (/-x2$/.test(String(type))) {
return spacePatterns[String(type).replace(/-x2$/, '')] * 2
}
return spacePatterns[type] || 0
return spacePatterns[String(type)] || 0
}

// Splits a string of: "large x-small" into an array of the same
export const splitTypes = (types) => {
// @internal Splits a string of: "large x-small" into an array of the same
export const splitTypes = (types: SpaceType | Array<SpaceType>) => {
if (typeof types === 'string') {
types = types.split(/ /g)
return clean(types.split(/ /g))
} else if (typeof types === 'boolean') {
return ['small']
} else if (typeof types === 'number') {
return [types]
}
return types ? types.filter((r) => r && r.length > 0) : null

return clean(types) || null

function clean(t: Array<SpaceType>) {
return t?.filter((r) => r && String(r).length > 0)
}
}

// Sums e.g. "large" + "x-small" to be = 2.5rem
export const sumTypes = (types) =>
// @internal Sums e.g. "large" + "x-small" to be = 2.5rem
export const sumTypes = (types: SpaceType | Array<SpaceType>) =>
splitTypes(types)
.map((type) => translateSpace(type))
.reduce((acc, cur) => {
Expand All @@ -67,20 +163,19 @@ export const sumTypes = (types) =>
return acc
}, 0)

// Returns an array with modifiers e.g. ["--large" + "--x-small"]
export const createTypeModifiers = (types) => {
if (typeof types === 'number') {
types = String(types)
}
// @internal Returns an array with modifiers e.g. ["large", "x-small"]
export const createTypeModifiers = (
types: SpaceType
): Array<SpaceType> => {
return (splitTypes(types) || []).reduce((acc, type) => {
if (type) {
const firstLetter = type[0]
const firstLetter = String(type)[0]
if (parseFloat(firstLetter) > -1) {
// can be "2rem" or "32px" - but we want only a number
let num = parseFloat(type)
let num = parseFloat(String(type))

// check if we got pixels
if (num >= 8 && /[0-9]px/.test(type)) {
if (num >= 8 && /[0-9]px/.test(String(type))) {
num = num / 16
}

Expand All @@ -99,53 +194,56 @@ export const createTypeModifiers = (types) => {
}
}

if (!(parseFloat(type) > 0)) {
if (!(parseFloat(String(type)) > 0)) {
acc.push(type)
}
}
return acc
}, [])
}

// Finds from "2.0" the equivalent type "large"
export const findType = (num, { returnObject = false } = {}) => {
const found =
Object.entries(spacePatterns).find(([k, v]) => k && v === num) || null

if (returnObject) {
return found
}
// @internal Finds from "2.0" the equivalent type "large"
export const findType = (num: SpaceNumber): SpaceType => {
const found = findTypeAll(num)

// get the type
if (found) {
return found[0]
}

return null
}

// @internal Finds from "2.0" the equivalent type "large" and returns all results
export const findTypeAll = (num: SpaceNumber): Array<SpaceType> => {
const found =
Object.entries(spacePatterns).find(([k, v]) => k && v === num) || null

return found
}

// Finds from e.g. a value of "2.5rem" the nearest type = ["large", "x-small"]
export const findNearestTypes = (num) => {
// @internal Finds from e.g. a value of "2.5rem" the nearest type = ["large", "x-small"]
export const findNearestTypes = (num: SpaceNumber, multiply = false) => {
let res = []

const near = Object.entries(spacePatterns)
.reverse()
.filter((k) => (multiply ? true : !k[0].includes('-x'))) // e.g. -x2
.find(([k, v]) => k && num >= v)
const nearNum = (near && near[1]) || num
const types = findTypeAll(nearNum)

const typeObject = findType(nearNum, { returnObject: true })

if (typeObject) {
const nearType = typeObject[0]
if (types) {
const nearType = types[0]
res.push(nearType)
const leftOver = num - parseFloat(String(typeObject[1]))
const foundMoreTypes = findNearestTypes(leftOver)
const leftOver = num - parseFloat(String(types[1]))
const foundMoreTypes = findNearestTypes(leftOver, multiply)

// if the value already exists, then replace it with an x2
foundMoreTypes.forEach((type) => {
const index = res.indexOf(type)
if (index !== -1) {
res[index] = `${type}-x2`
res[index] = multiply ? `${type}-x2` : type
}
})

Expand All @@ -155,11 +253,11 @@ export const findNearestTypes = (num) => {
return res
}

// Checks if a space prop is a valid string like "top"
// @internal Checks if a space prop is a valid string like "top"
export const isValidSpaceProp = (propName: string) =>
propName && ['top', 'right', 'bottom', 'left'].includes(propName)

export const removeSpaceProps = (props: Props) => {
export const removeSpaceProps = (props: StyleObjectProps) => {
const p = Object.isFrozen(props) ? { ...props } : props
for (const i in p) {
if (isValidSpaceProp(i)) {
Expand All @@ -169,68 +267,8 @@ export const removeSpaceProps = (props: Props) => {
return p
}

// Creates a valid space CSS class out from given space types
export const createSpacingClasses = (props: Props, Element = null) => {
const p = Object.isFrozen(props) ? { ...props } : props

if (typeof p.space !== 'undefined') {
if (
typeof p.space === 'string' ||
typeof p.space === 'number' ||
(typeof p.space === 'boolean' && p.space)
) {
p.top = p.right = p.bottom = p.left = p.space
}
if (typeof p.space === 'object') {
for (const i in p.space) {
if (!p[i] && isValidSpaceProp(i)) {
p[i] = p.space[i]
}
}
}
delete p.space
}

return Object.entries(p).reduce((acc, [direction, cur]) => {
if (isValidSpaceProp(direction)) {
if (String(cur) === '0' || String(cur) === 'false') {
acc.push(`dnb-space__${direction}--zero`)
} else if (cur) {
const typeModifiers = createTypeModifiers(cur)

// get the total sum
const sum = sumTypes(typeModifiers)
if (sum > 10) {
warn(
`Spacing of more than 10rem is not supported! You used ${sum} / (${typeModifiers.join(
','
)})`
)
} else {
// auto combine classes
const nearestTypes = findNearestTypes(sum)

acc = [
...acc,
...nearestTypes.map(
(type) => `dnb-space__${direction}--${type}`
),
]
}
}
} else if (direction === 'no_collapse') {
acc.push('dnb-space--no-collapse')
if (Element && isInline(Element)) {
acc.push('dnb-space--inline')
}
}

return acc
}, [])
}

// deprecated and can be removed in v10 (remove tests as well)
export const createStyleObject = (props: Props & StyleObjectProps) => {
export const createStyleObject = (props: StyleObjectProps) => {
const p = Object.isFrozen(props) ? { ...props } : props

if (p.top && !(parseFloat(String(p.top)) > 0)) {
Expand Down
Loading

0 comments on commit f6ae0a9

Please sign in to comment.