Skip to content

Commit 817449f

Browse files
committed
proper border support
1 parent bd1426e commit 817449f

File tree

9 files changed

+814
-127
lines changed

9 files changed

+814
-127
lines changed

.babelrc

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
{
22
"presets": [
33
"@babel/react",
4-
"@babel/env"
5-
]
4+
"@babel/env",
5+
"minify"
6+
],
7+
"plugins": [
8+
"@babel/plugin-proposal-class-properties"
9+
],
10+
"comments": false
611
}

package-lock.json

Lines changed: 515 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,20 @@
2323
},
2424
"homepage": "https://github.com/drinking-code/react-round-div#readme",
2525
"dependencies": {
26-
"react": "^17.0.2"
26+
"prop-types": "^15.7.2",
27+
"react": "^17.0.2",
28+
"react-dom": "^17.0.2"
2729
},
2830
"devDependencies": {
2931
"@babel/cli": "^7.13.14",
3032
"@babel/core": "^7.13.15",
33+
"@babel/plugin-proposal-class-properties": "^7.13.0",
3134
"@babel/preset-env": "^7.13.15",
3235
"@babel/preset-react": "^7.13.13",
3336
"babel-core": "^7.0.0-bridge.0",
3437
"babel-jest": "^26.6.3",
38+
"babel-preset-minify": "^0.5.1",
3539
"jest": "^26.6.3",
36-
"react-dom": "^17.0.2",
3740
"regenerator-runtime": "^0.13.7"
3841
}
3942
}

src/css-utils.js

Lines changed: 62 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,73 @@
11
import CSS_COLOR_NAMES from "./html-colors";
22

3-
function getColor(border) {
4-
if (!border) return false
5-
border = border.split(' ')
6-
for (let i in border) {
7-
i = Number(i)
8-
const val = border[i]
9-
// color is a hex code
10-
if (val.toLowerCase().match(/#([0-9a-f]{3}){1,2}/)) return val
11-
// color is a function (rgb, rgba, hsl, hsla)
12-
if (val.startsWith('rgb') || val.startsWith('hsl')) {
13-
if (val.endsWith(')')) return val
14-
let color = val;
3+
function _getAttributeFromString(string, method) {
4+
if (!string) return false
5+
string = string.split(' ')
6+
for (let i in string) {
7+
const res = method(string, Number(i))
8+
if (res) return res
9+
}
10+
}
11+
12+
function _getColor(border, i) {
13+
const val = border[i]
14+
// color is a hex code
15+
if (val.toLowerCase().match(/#([0-9a-f]{3}){1,2}/)) return val
16+
// color is a function (rgb, rgba, hsl, hsla)
17+
if (val.startsWith('rgb') || val.startsWith('hsl')) {
18+
let color = val;
19+
if (!val.endsWith(')'))
1520
for (let j = 1; !border[i + j - 1].endsWith(')'); j++) {
1621
color += border[i + j]
1722
}
18-
return color
19-
}
20-
// color is a html color name
21-
if (
22-
CSS_COLOR_NAMES.map(color => color.toLowerCase())
23-
.includes(val.toLowerCase())
24-
) return val
23+
if (color[3] === 'a')
24+
color = color.replace('a', '').replace(/,[^),]+\)/, ')')
25+
return color
2526
}
27+
// color is a html color name
28+
if (
29+
CSS_COLOR_NAMES.map(color => color.toLowerCase())
30+
.includes(val.toLowerCase())
31+
) return val
32+
return false
2633
}
2734

28-
const htmlLengthNotSvgError = new Error('Border length must be either thin, medium, thick, or in one of the following units: em, ex, cm, in, mm, px, pc, pt.')
29-
30-
function getWidth(border) {
31-
if (!border) return false
32-
border = border.split(' ')
33-
for (let i in border) {
34-
i = Number(i)
35-
const val = border[i]
36-
// width is 0
37-
if (val === '0') return '0'
38-
// width is a word
39-
if (val.toLowerCase() === 'thin') return '1px'
40-
if (val.toLowerCase() === 'medium') return '3px'
41-
if (val.toLowerCase() === 'thick') return '5px'
42-
if (val.match(/(cap|ch|ic|lh|rem|rlh|vh|vw|vi|vb|vmin|vmax|Q)/g)) throw htmlLengthNotSvgError
43-
// width is <length>
44-
if (val.match(/(\d+(\.\d+)?(em|ex|px|cm|mm|in|pc|pt)|0)/g)) return val
35+
function _getOpacity(border, i) {
36+
let val = border[i]
37+
if (val.startsWith('rgba') || val.startsWith('hsla')) {
38+
if (!val.endsWith(')'))
39+
for (let j = 1; !border[i + j - 1].endsWith(')'); j++) {
40+
val += border[i + j]
41+
}
42+
return val.replace(/(rgb|hsl)a?\(([^,)]+,){3}/, '').replace(/\)$/, '')
4543
}
44+
if (border.length - 1 === i)
45+
return 1
4646
}
4747

48-
export {getWidth, getColor}
48+
const htmlLengthNotSvgError = new Error('<RoundDiv> Border lengths must be either "thin", "medium", "thick", or in one of the following units: em, ex, cm, in, mm, px, pc, pt.')
49+
50+
function unitCheck(length) {
51+
if (length?.match(/(cap|ch|ic|lh|rem|rlh|vh|vw|vi|vb|vmin|vmax|Q)/g)) throw htmlLengthNotSvgError
52+
return length
53+
}
54+
55+
function _getWidth(border, i) {
56+
const val = border[i]
57+
// width is 0
58+
if (val === '0') return '0'
59+
// width is a word
60+
if (val.toLowerCase() === 'thin') return '1px'
61+
if (val.toLowerCase() === 'medium') return '3px'
62+
if (val.toLowerCase() === 'thick') return '5px'
63+
unitCheck(val)
64+
// width is <length>
65+
if (val.match(/(\d+(\.\d+)?(em|ex|px|cm|mm|in|pc|pt)|0)/g)) return val
66+
return false
67+
}
68+
69+
const getWidth = s => _getAttributeFromString(s, _getWidth),
70+
getColor = s => _getAttributeFromString(s, _getColor),
71+
getOpacity = s => _getAttributeFromString(s, _getOpacity)
72+
73+
export {getWidth, getColor, unitCheck, getOpacity}

src/getMatchedCSSRules-polyfill.js

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,21 @@
1-
// polyfill window.getMatchedCSSRules() in FireFox 6+
21
/* eslint-disable */
2+
// polyfill element.matches
3+
if (!Element.prototype.matches) {
4+
Element.prototype.matches =
5+
Element.prototype.matchesSelector ||
6+
Element.prototype.mozMatchesSelector ||
7+
Element.prototype.msMatchesSelector ||
8+
Element.prototype.oMatchesSelector ||
9+
Element.prototype.webkitMatchesSelector ||
10+
function(s) {
11+
var matches = (this.document || this.ownerDocument).querySelectorAll(s),
12+
i = matches.length;
13+
while (--i >= 0 && matches.item(i) !== this) {}
14+
return i > -1;
15+
};
16+
}
17+
18+
// polyfill window.getMatchedCSSRules() in FireFox 6+
319
if ( typeof window.getMatchedCSSRules !== 'function' ) {
420
var ELEMENT_RE = /[\w-]+/g,
521
ID_RE = /#[\w-]+/g,
@@ -72,7 +88,7 @@ if ( typeof window.getMatchedCSSRules !== 'function' ) {
7288
var selectors = selector_text.split(','),
7389
selector, score, result = 0;
7490
while ( selector = selectors.shift() ) {
75-
if ( element.mozMatchesSelector(selector) ) {
91+
if ( element.matches(selector) ) {
7692
score = calculateScore(selector);
7793
result = score > result ? score : result;
7894
}
@@ -121,7 +137,7 @@ if ( typeof window.getMatchedCSSRules !== 'function' ) {
121137
}
122138
//TODO: for now only polyfilling Gecko
123139
// check if this element matches this rule's selector
124-
if ( element.mozMatchesSelector(rule.selectorText) ) {
140+
if ( element.matches(rule.selectorText) ) {
125141
// push the rule to the results set
126142
result.push(rule);
127143
}

src/main.js

Lines changed: 54 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,106 +1,83 @@
11
import React, {useRef, useEffect, useState} from 'react';
2-
import getStyle from "./styles-extractor";
32
import './getMatchedCSSRules-polyfill'
4-
import {getWidth, getColor} from "./css-utils";
3+
import updateStates from "./updateStates";
4+
import ShadowRoot from "./react-shadow-dom";
55

66
export default function RoundDiv({clip, style, children, ...props}) {
77
const [height, setHeight] = useState(0)
88
const [width, setWidth] = useState(0)
9-
const [offsetX, setOffsetX] = useState(0)
10-
const [offsetY, setOffsetY] = useState(0)
119
const [radius, setRadius] = useState(0)
1210
const [background, setBackground] = useState('transparent')
11+
const [backgroundOpacity, setBackgroundOpacity] = useState(0)
1312
const [borderColor, setBorderColor] = useState('transparent')
1413
const [borderWidth, setBorderWidth] = useState('1')
14+
const [borderOpacity, setBorderOpacity] = useState(1)
1515

1616
const div = useRef()
1717

1818
useEffect(() => {
1919
// attach shadow root to div
20-
if (div.current?.shadowRoot) return
21-
const shadow = div.current?.attachShadow({mode: 'open'})
22-
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
23-
svg.style.position = 'fixed';
24-
svg.style.left = '0px';
25-
svg.style.top = '0px';
26-
svg.style.height = '0px';
27-
svg.style.width = '0px';
28-
svg.style.overflow = 'visible';
29-
svg.style.zIndex = '-1';
30-
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path')
31-
svg.appendChild(path)
32-
shadow.appendChild(svg)
33-
const content = document.createElement('slot')
34-
shadow.appendChild(content)
20+
if (!div.current?.shadowRoot)
21+
div.current?.attachShadow({mode: 'open'})
3522
}, [])
3623

37-
useEffect(() => {
38-
const boundingClientRect = div.current?.getBoundingClientRect()
39-
if (boundingClientRect) {
40-
setHeight(boundingClientRect.height)
41-
setWidth(boundingClientRect.width)
42-
setOffsetX(boundingClientRect.left)
43-
setOffsetY(boundingClientRect.top)
44-
}
45-
const divStyle = boundingClientRect ? window?.getComputedStyle(div.current) : null
46-
if (divStyle) {
47-
setRadius(Number(divStyle.borderRadius.replace('px', '')))
48-
setBackground(
49-
style?.background
50-
|| style?.backgroundColor
51-
|| getStyle('background', div.current)?.overwritten[1]?.value
52-
|| 'transparent'
53-
)
54-
setBorderColor(
55-
getColor(style?.border)
56-
|| style?.borderColor
57-
|| getStyle('borderColor', div.current)?.overwritten[1]?.value
58-
|| 'transparent'
59-
)
60-
setBorderWidth(
61-
getWidth(style?.border)
62-
|| style?.borderWidth
63-
|| getStyle('borderWidth', div.current)?.overwritten[1]?.value
64-
|| '1'
65-
)
66-
}
67-
}, [div, clip, style])
68-
69-
useEffect(() => {
70-
const path = div.current?.shadowRoot?.querySelector('path')
71-
if (!path) return
72-
path.parentNode.style.width = width
73-
path.parentNode.style.height = height
74-
path.parentNode.style.top = offsetY
75-
path.parentNode.style.left = offsetX
76-
path.parentNode.removeAttributeNS('http://www.w3.org/2000/svg', 'viewBox')
77-
path.parentNode.setAttributeNS(
78-
'http://www.w3.org/2000/svg',
79-
'viewBox',
80-
`0 0 ${height} ${width}`
81-
)
82-
const newPath = document.createElementNS('http://www.w3.org/2000/svg', 'path')
83-
newPath.setAttributeNS(
84-
'http://www.w3.org/2000/svg',
85-
'd',
86-
generateSvgSquircle(height, width, radius, clip)
87-
)
88-
newPath.setAttributeNS('http://www.w3.org/2000/svg', 'fill', background)
89-
newPath.setAttributeNS('http://www.w3.org/2000/svg', 'stroke', borderColor)
90-
newPath.setAttributeNS('http://www.w3.org/2000/svg', 'stroke-width', borderWidth)
91-
// rerender
92-
path.parentNode.innerHTML = newPath.outerHTML
93-
}, [background, height, width, radius, clip, offsetX, offsetY, borderColor, borderWidth])
24+
useEffect(() => updateStates({
25+
div,
26+
style,
27+
setHeight,
28+
setWidth,
29+
setRadius,
30+
setBackground,
31+
setBackgroundOpacity,
32+
setBorderColor,
33+
setBorderWidth,
34+
setBorderOpacity
35+
}), [div, clip, style])
9436

9537
const divStyle = {
9638
...style
9739
}
9840

9941
divStyle.background = 'transparent'
100-
divStyle.border = divStyle.border || '0'
42+
divStyle.borderWidth = divStyle.borderWidth || '0'
10143
divStyle.borderColor = 'transparent'
10244

10345
return <div {...props} style={divStyle} ref={div}>
46+
<ShadowRoot>
47+
<style>{`
48+
rect {
49+
width: calc(${width}px + ${borderWidth} * 2);
50+
height: calc(${height}px + ${borderWidth} * 2);
51+
x: calc(${borderWidth} * -1);
52+
y: calc(${borderWidth} * -1);
53+
}
54+
#border {
55+
stroke-width: ${borderWidth};
56+
}
57+
`}
58+
</style>
59+
<svg viewBox={`0 0 ${height} ${width}`} style={{
60+
position: 'fixed',
61+
height,
62+
width,
63+
overflow: 'visible',
64+
zIndex: -1
65+
}} xmlnsXlink="http://www.w3.org/1999/xlink">
66+
<defs>
67+
<path d={generateSvgSquircle(height, width, radius, clip)} id="shape"/>
68+
69+
<mask id="outsideOnly">
70+
<rect fill="white"/>
71+
<use xlinkHref="#shape" fill="black"/>
72+
</mask>
73+
</defs>
74+
75+
<use xlinkHref="#shape" id="border" stroke={borderColor} fill="none"
76+
opacity={borderOpacity} mask="url(#outsideOnly)"/>
77+
<use xlinkHref="#shape" fill={background} opacity={backgroundOpacity}/>
78+
</svg>
79+
<slot style={{zIndex: 1}}/>
80+
</ShadowRoot>
10481
{children}
10582
</div>
10683
}

0 commit comments

Comments
 (0)