copy(props.token)}
10 | css={{
11 | cursor: "pointer"
12 | }}
13 | {...props}
14 | />
15 | )
16 | }
17 |
18 | Swatch.propTypes = {
19 | ...tokenPropType,
20 | ...valuePropType
21 | }
22 |
23 | export default Swatch
24 |
--------------------------------------------------------------------------------
/packages/react-design-tokens/src/Swatch.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/component-driven/ui/88e9340ef39b4e90c88fcec9a73138edb94d258e/packages/react-design-tokens/src/Swatch.md
--------------------------------------------------------------------------------
/packages/react-design-tokens/src/SwatchToken.jsx:
--------------------------------------------------------------------------------
1 | /* @jsx jsx */
2 | import { jsx } from "theme-ui";
3 | import React from "react";
4 |
5 | function SwatchToken({ color = "text", css: componentCSS, ...rest }) {
6 | return (
7 |
18 | );
19 | }
20 |
21 | export default SwatchToken;
22 |
--------------------------------------------------------------------------------
/packages/react-design-tokens/src/SwatchToken.md:
--------------------------------------------------------------------------------
1 | A primitive to render the token name.
2 |
3 | ```jsx harmony
4 |
Token name
5 | ```
6 |
7 | Color can be customized using `color` prop:
8 |
9 | ```jsx harmony
10 |
Primary token
11 | ```
12 |
13 | For more customization use `css` and `style` props
14 |
15 | ```jsx harmony
16 |
24 | Custom style
25 |
26 | ```
27 |
--------------------------------------------------------------------------------
/packages/react-design-tokens/src/SwatchValue.jsx:
--------------------------------------------------------------------------------
1 | /* @jsx jsx */
2 | import { jsx } from "theme-ui";
3 | import React from "react";
4 |
5 | function SwatchValue({ color = "secondary", css: componentCSS, ...rest }) {
6 | return (
7 |
18 | );
19 | }
20 |
21 | export default SwatchValue;
22 |
--------------------------------------------------------------------------------
/packages/react-design-tokens/src/SwatchValue.md:
--------------------------------------------------------------------------------
1 | A primitive to render token's value
2 |
3 | ```jsx harmony
4 |
13px
5 | ```
6 |
7 | Use `color` prop to customize color:
8 |
9 | ```jsx harmony
10 |
24px
11 | ```
12 |
13 | or `css` and `style` to customize everything else:
14 |
15 | ```jsx harmony
16 |
24 | Custom value
25 |
26 | ```
27 |
--------------------------------------------------------------------------------
/packages/react-design-tokens/src/Swatches.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 |
4 | const Swatches = ({ items = [], children }) => (
5 | <>
6 | {Array.isArray(items)
7 | ? items.map((value, index) => children(index, value))
8 | : Object.keys(items).map(key => children(key, items[key]))}
9 | >
10 | );
11 |
12 | Swatches.propTypes = {
13 | items: PropTypes.oneOfType([PropTypes.array, PropTypes.object]).isRequired,
14 | children: PropTypes.func.isRequired
15 | };
16 |
17 | /** @component */
18 | export default Swatches;
19 |
--------------------------------------------------------------------------------
/packages/react-design-tokens/src/Swatches.md:
--------------------------------------------------------------------------------
1 | ```jsx harmony
2 | import { Grid } from "theme-ui"
3 | import theme from "./theme"
4 | import { Swatch, Swatches, SwatchToken, SpacingSwatch } from "."
5 | ;
6 |
7 | {(token, value) => (
8 |
9 | {token}
10 |
11 |
12 | )}
13 |
14 |
15 | ```
16 |
--------------------------------------------------------------------------------
/packages/react-design-tokens/src/TextStyleSwatch.jsx:
--------------------------------------------------------------------------------
1 | /* @jsx jsx */
2 | import { jsx } from "theme-ui"
3 | import React from "react"
4 | import { valuePropType } from "./propTypes"
5 |
6 | /**
7 | * A swatch to render a `textStyle` variant. Provide the sample text as `children`.
8 | * @param value
9 | * @param componentCSS
10 | * @param rest
11 | * @return React.Element
12 | * @constructor
13 | */
14 | const TextStyleSwatch = ({ value, css: componentCSS, ...rest }) => (
15 |
24 | )
25 |
26 | TextStyleSwatch.propTypes = {
27 | ...valuePropType
28 | }
29 |
30 | /** @component */
31 | export default TextStyleSwatch
32 |
--------------------------------------------------------------------------------
/packages/react-design-tokens/src/TextStyleSwatch.md:
--------------------------------------------------------------------------------
1 | ```jsx harmony
2 | import theme from "./theme"
3 | ;
4 | Aa
5 |
6 | ```
7 |
--------------------------------------------------------------------------------
/packages/react-design-tokens/src/Typography.jsx:
--------------------------------------------------------------------------------
1 | /* @jsx jsx */
2 | import { Grid, jsx, ThemeProvider } from "theme-ui"
3 | import React from "react"
4 | import { Swatch, Swatches, SwatchToken, TextStyleSwatch } from "."
5 |
6 | /**
7 | * Typography component showcases all available text styles defined in `theme.textStyles`
8 | * object of [styled-system theme](https://styled-system.com/table#variants).
9 | * @param theme
10 | * @return React.Element
11 | * @constructor
12 | */
13 | export default function Typography({ theme }) {
14 | return (
15 |
16 |
17 |
18 | {(token, value) => (
19 |
20 |
21 |
27 | {token}
28 |
29 |
30 | The quick brown fox jumps over the lazy dog
31 |
32 |
33 |
34 | )}
35 |
36 |
37 |
38 | )
39 | }
40 |
--------------------------------------------------------------------------------
/packages/react-design-tokens/src/Typography.md:
--------------------------------------------------------------------------------
1 | ```jsx harmony
2 | import theme from "./theme"
3 | ;
4 | ```
5 |
6 | Starting from v5 of styled-system, you can co-locate variants inside your components.
7 |
8 | Consider this is your `Text` component that defines text styles:
9 |
10 | ```js { "file": "../../examples/Text.js" }
11 |
12 | ```
13 |
14 | ```jsx harmony
15 | import theme from "./theme"
16 | import { textStyles } from "./examples/Text"
17 | ;
18 | ```
19 |
--------------------------------------------------------------------------------
/packages/react-design-tokens/src/examples/Text.js:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled'
2 | import { css } from '@styled-system/css'
3 | import { typography, variant, compose } from 'styled-system'
4 |
5 | export const textStyles = {
6 | body: {
7 | fontFamily: 'body',
8 | fontSize: 'md',
9 | fontWeight: 'normal',
10 | color: 'text'
11 | },
12 | heading: {
13 | fontFamily: 'heading',
14 | fontSize: 'xl',
15 | fontWeight: 'bold',
16 | color: 'secondary'
17 | }
18 | }
19 |
20 | const Text = styled.p(
21 | css({
22 | m: 0,
23 | lineHeight: 1.5
24 | }),
25 | compose(
26 | variant({
27 | prop: 'textStyle',
28 | variants: textStyles
29 | }),
30 | typography
31 | )
32 | )
33 |
34 | export default Text
35 |
--------------------------------------------------------------------------------
/packages/react-design-tokens/src/examples/cdds.js:
--------------------------------------------------------------------------------
1 | import { modularScale } from 'polished'
2 |
3 | const scale = (value) => modularScale(value, '1rem', 'goldenSection')
4 |
5 | const fontSizes = {
6 | xl: scale(3),
7 | lg: scale(1),
8 | md: scale(0),
9 | sm: scale(-0.5),
10 | xs: scale(-0.75)
11 | }
12 |
13 | const palette = {
14 | grey: [
15 | 'rgb(255, 255, 255)',
16 | 'rgb(250, 250, 250)',
17 | 'rgb(246, 246, 246)',
18 | 'rgb(225, 225, 225)',
19 | 'rgb(187, 187, 187)',
20 | 'rgb(126, 126, 126)',
21 | 'rgb(51, 51, 51)'
22 | ],
23 | purple: [
24 | 'rgb(255, 230, 242)',
25 | 'rgb(251, 209, 234)',
26 | 'rgb(248, 188, 229)',
27 | 'rgb(231, 143, 222)',
28 | 'rgb(189, 96, 200)',
29 | 'rgb(120, 51, 150)',
30 | 'rgb(52, 18, 90)'
31 | ]
32 | }
33 |
34 | let invertedPalette = {}
35 |
36 | Object.keys(palette).forEach((key) => {
37 | invertedPalette[key] = [...palette[key]].reverse()
38 | })
39 |
40 | function getColors(palette) {
41 | return {
42 | ...palette,
43 | bg: palette.grey[0],
44 | base: palette.grey[6],
45 | primary: palette.purple[5],
46 | secondary: palette.grey[5],
47 | muted: palette.grey[2],
48 | hover: palette.purple[2],
49 | focus: palette.purple[1],
50 | error: '#d0453e',
51 | rating: '#f8c124'
52 | }
53 | }
54 |
55 | const theme = {
56 | fonts: {
57 | body: 'Helvetica Neue, Helvetica, Arial, sans-serif',
58 | heading: 'Helvetica Neue, Helvetica, Arial, sans-serif',
59 | monospace: 'Menlo, monospace'
60 | },
61 | fontSizes: {
62 | base: fontSizes.md,
63 | ...fontSizes
64 | },
65 | fontWeights: {
66 | normal: 400,
67 | bold: 700
68 | },
69 | headingFontWeights: {
70 | xl: 400,
71 | l: 400,
72 | m: 700
73 | },
74 | lineHeights: {
75 | base: 1.5,
76 | heading: 1.1
77 | },
78 | palette,
79 | colors: getColors(palette),
80 | borders: {
81 | none: 'none',
82 | thin: '1px solid'
83 | },
84 | radii: {
85 | base: '0.15em'
86 | },
87 | space: [
88 | 0,
89 | '0.125rem', // 2px
90 | '0.25rem', // 4px
91 | '0.5rem', // 8px
92 | '1rem', // 16px
93 | '2rem', // 32px
94 | '4rem', // 64px
95 | '8rem', // 128px
96 | '16rem' // 256px
97 | ],
98 | textStyles: {
99 | base: {},
100 | secondary: {
101 | color: palette.grey[5]
102 | },
103 | tertiary: {
104 | color: palette.grey[5],
105 | fontSize: fontSizes.s
106 | },
107 | error: {
108 | color: getColors(palette).error
109 | }
110 | }
111 | }
112 |
113 | export default theme
114 |
115 | export const inverted = {
116 | ...theme,
117 | colors: {
118 | ...getColors(invertedPalette),
119 | primary: invertedPalette.grey[4],
120 | hover: invertedPalette.grey[6],
121 | focus: invertedPalette.grey[1]
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/packages/react-design-tokens/src/examples/primer.js:
--------------------------------------------------------------------------------
1 | const colors = require('primer-colors')
2 | const space = require('primer-spacing')
3 | const { fontSizes, lineHeights } = require('primer-typography')
4 |
5 | const theme = {
6 | colors,
7 | space,
8 | fontSizes,
9 | lineHeights
10 | }
11 |
12 | module.exports = theme
13 |
--------------------------------------------------------------------------------
/packages/react-design-tokens/src/index.js:
--------------------------------------------------------------------------------
1 | export { default as Swatches } from "./Swatches"
2 | export { default as Swatch } from "./Swatch"
3 | export { default as SwatchToken } from "./SwatchToken"
4 | export { default as SwatchValue } from "./SwatchValue"
5 | export { default as ColorSwatch } from "./ColorSwatch"
6 | export { default as TextStyleSwatch } from "./TextStyleSwatch"
7 | export { default as PaletteSwatch } from "./PaletteSwatch"
8 | export { default as SpacingSwatch } from "./SpacingSwatch"
9 | export { default as Colors } from "./Colors"
10 | export { default as Typography } from "./Typography"
11 | export { default as Spacing } from "./Spacing"
12 |
--------------------------------------------------------------------------------
/packages/react-design-tokens/src/propTypes.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types'
2 |
3 | /**
4 | * Design token name
5 | * See https://theme-ui.com/theme-spec
6 | * */
7 | export const tokenPropType = {
8 | token: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired
9 | }
10 |
11 | /**
12 | * The value to render inside the swatch
13 | * */
14 | export const valuePropType = {
15 | value: PropTypes.oneOfType([
16 | PropTypes.string,
17 | PropTypes.number,
18 | PropTypes.object,
19 | PropTypes.array
20 | ]).isRequired
21 | }
22 |
--------------------------------------------------------------------------------
/packages/react-design-tokens/src/theme.js:
--------------------------------------------------------------------------------
1 | import { modularScale } from 'polished'
2 |
3 | const scale = (value) => modularScale(value, '1rem', 'goldenSection')
4 |
5 | const palette = {
6 | slate: {
7 | darker: '#11161A',
8 | dark: '#1F2932',
9 | base: '#2E3D49',
10 | light: '#6D7780',
11 | lighter: '#B4B9BD',
12 | lightest: '#F7F7F8'
13 | }
14 | }
15 | export default {
16 | fontSizes: {
17 | xl: scale(3),
18 | lg: scale(1),
19 | md: scale(0),
20 | sm: scale(-0.5),
21 | xs: scale(-0.75)
22 | },
23 | fonts: {
24 | body: 'system-ui, sans-serif',
25 | heading: '"Avenir Next", sans-serif',
26 | monospace: 'Menlo, monospace'
27 | },
28 | textStyles: {
29 | body: {
30 | fontFamily: 'body',
31 | fontSize: 'md',
32 | color: 'text'
33 | },
34 | heading: {
35 | fontSize: 'xl',
36 | fontFamily: 'heading',
37 | color: 'secondary'
38 | }
39 | },
40 | space: [
41 | 0,
42 | '0.125rem', // 2px
43 | '0.25rem', // 4px
44 | '0.5rem', // 8px
45 | '1rem', // 16px
46 | '2rem', // 32px
47 | '4rem', // 64px
48 | '8rem', // 128px
49 | '16rem' // 256px
50 | ],
51 | colors: {
52 | ...palette,
53 | text: palette.slate.base,
54 | background: palette.slate.lightest,
55 | primary: '#E53935',
56 | secondary: palette.slate.light,
57 | muted: palette.slate.lighter
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/packages/react-focus-within/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # @component-driven/react-focus-within
2 |
3 | ## 2.0.2
4 |
5 | ### Patch Changes
6 |
7 | - 43e732f: Bump prismjs from 1.20.0 to 1.21.0
8 | - b527d04: Bump lodash from 4.17.15 to 4.17.19
9 | - eaea858: Bump elliptic from 6.5.2 to 6.5.3
10 | - 7952d76: Bump markdown-to-jsx from 6.11.1 to 6.11.4
11 | - 5e71065: Bump http-proxy from 1.18.0 to 1.18.1
12 |
13 | ## 2.0.1
14 |
15 | ### Patch Changes
16 |
17 | - 25dfdc1: Fix typo in 'Using with CSS-in-JS libs' section for '@component-driven/react-focus-within' package.
18 |
19 | ## 2.0.0
20 |
21 | ### Major Changes
22 |
23 | - a2dbfb1: Switch to a monorepo setup and use [Changesets](https://github.com/atlassian/changesets).
24 |
25 | Each package is a separate npm package.
26 |
--------------------------------------------------------------------------------
/packages/react-focus-within/Readme.md:
--------------------------------------------------------------------------------
1 | FocusWithin is a component that allows detecting if one of its children has focus. It can be considered as a missing JS API for `focus-within`.
2 |
3 | FocusWithin will fire `onFocus` once one of its children will receive focus. Similarly `onBlur` is going to be fired once focus is left all its children.
4 |
5 | ## Simple example
6 |
7 | Open developer console to see log messages.
8 |
9 | ```jsx
10 |
{
12 | console.log('Received focus')
13 | }}
14 | onBlur={() => {
15 | console.log('Lost focus')
16 | }}
17 | >
18 |
19 |
20 |
21 |
22 | ```
23 |
24 | ## Reacting to the focus change
25 |
26 | If you want to react to the focus change, use function as a children pattern. When function is used as children, you _must_ provide the `ref` prop.
27 |
28 | ```jsx
29 |
{
31 | console.log('Received focus')
32 | }}
33 | onBlur={() => {
34 | console.log('Lost focus')
35 | }}
36 | >
37 | {({ focused, getRef }) => (
38 |
61 | )}
62 |
63 | ```
64 |
65 | ## Using with CSS-in-JS libs
66 |
67 | If you're using a CSS-in-JS library like [styled-components](https://www.styled-components.com) you need to pass a ref using `ref` prop. You can use `getRef` function from the parameters.
68 |
69 | ```js static
70 | ;({ focused: Boolean, getRef: Function }) => React.Element
71 | ```
72 |
73 | ```jsx
74 | const styled = require('styled-components').default
75 | const StyledBox = styled('div')`
76 | padding: 20px;
77 | border: 1px solid;
78 | border-color: ${props =>
79 | props.focused ? 'palevioletred' : '#999'};
80 |
81 | & > * + * {
82 | margin-left: 20px;
83 | }
84 | `
85 | ;
{
87 | console.log('Received focus')
88 | }}
89 | onBlur={() => {
90 | console.log('Lost focus')
91 | }}
92 | >
93 | {({ focused, getRef }) => (
94 |
95 |
99 |
103 |
104 |
105 | )}
106 |
107 | ```
108 |
109 | _Note:_ It's recommended to use `:focus-within` selector instead of interpoaltions whenever possible.
110 |
111 | ## Focus method
112 |
113 | Sometimes it's needed to focus the container node programmatically. You can use the public method `focus`. Note that `tabIndex={-1}` needs to be set on non-interactive elements to make them receive focus.
114 |
115 | ```jsx
116 | const ref = React.createRef()
117 | ;
118 |
119 | {({ focused, getRef }) => (
120 |
121 | {focused ? 'Focused' : 'Not focused'}
122 |
123 | )}
124 |
125 |
132 |
133 | ```
134 |
135 | ## Naïve focus trap implementation
136 |
137 | ```jsx
138 | import { useRef, useState, useEffect } from 'react'
139 |
140 | const firstInput = useRef(null)
141 | const [enabled, setEnabled] = useState(false)
142 |
143 | useEffect(() => {
144 | if (enabled) {
145 | firstInput.current.focus()
146 | }
147 | }, [enabled])
148 | ;
{
150 | enabled && firstInput.current.focus()
151 | }}
152 | >
153 |
169 |
170 | ```
171 |
--------------------------------------------------------------------------------
/packages/react-focus-within/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@component-driven/react-focus-within",
3 | "description": "The missing JS API of the CSS `:focus-within` for React",
4 | "version": "2.0.2",
5 | "main": "dist/react-focus-within.cjs.js",
6 | "module": "dist/react-focus-within.esm.js",
7 | "dependencies": {
8 | "prop-types": "^15.6.2"
9 | },
10 | "peerDependencies": {
11 | "react": ">=16.0",
12 | "react-dom": ">=16.0"
13 | },
14 | "author": "Andrey Okonetchnikov
",
15 | "license": "MIT",
16 | "publishConfig": {
17 | "access": "public"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/packages/react-focus-within/src/FocusWithin.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node, browser */
2 |
3 | import React from 'react'
4 | import PropTypes from 'prop-types'
5 |
6 | const noOutlineStyles = {
7 | outline: 'none'
8 | }
9 |
10 | class FocusWithin extends React.Component {
11 | state = {
12 | focused: false
13 | }
14 |
15 | _isMounted = false
16 | _lastBlurEvent = null
17 |
18 | ref = React.createRef()
19 |
20 | componentDidMount() {
21 | /**
22 | * In order for document.body to receive focus events
23 | * it needs to be focusable. Adding `tabindex="-1"` makes it focusable
24 | * but prevents it from receiving the focus on user interaction.
25 | */
26 | if (document != null) {
27 | document.querySelector('body').setAttribute('tabindex', '-1')
28 | }
29 | /* Mark as mounted */
30 | this._isMounted = true
31 | }
32 |
33 | componentWillUnmount() {
34 | /* Mark as unmounted */
35 | this._isMounted = false
36 |
37 | /* Since the onBlur for the unmounted component will never fire, we need to cleanup here. */
38 | document.removeEventListener('focusin', this._onFocusIn)
39 | }
40 |
41 | /**
42 | * Calls `focus` method on the container node
43 | *
44 | * @public
45 | * @method focus
46 | * */
47 | focus() {
48 | const node = this.ref.current
49 | if (node != null && typeof node.focus === 'function') {
50 | node.focus()
51 | }
52 | }
53 |
54 | /**
55 | * Event handler that fires if the FocusEvent bubbled up to the document.
56 | *
57 | * @private
58 | * @method _onFocusIn
59 | *
60 | * We check if 3 conditions are met:
61 | * 1. Current state is focused
62 | * 2. Blur occured inside the container
63 | * 3. Focus occured outside of the container
64 | * 4. Component is still mounted to the DOM
65 | *
66 | * In this case we fire `onBlur` callback.
67 | */
68 | _onFocusIn = () => {
69 | if (
70 | this._isMounted &&
71 | this._lastBlurEvent &&
72 | this.isInsideNode(this.ref.current, this._lastBlurEvent.target) &&
73 | !this.isInsideNode(this.ref.current, document.activeElement)
74 | ) {
75 | this.setState(
76 | {
77 | focused: false
78 | },
79 | () => {
80 | document.removeEventListener('focusin', this._onFocusIn)
81 | this.props.onBlur(this._lastBlurEvent)
82 | }
83 | )
84 | }
85 | }
86 |
87 | /**
88 | * @private
89 | * @method onFocus
90 | */
91 | onFocus = evt => {
92 | const { onFocus } = this.props
93 | const { focused } = this.state
94 |
95 | /**
96 | * If it's not focused yet we'll set the state to `focused: true`
97 | */
98 | if (!focused) {
99 | this.setState(
100 | {
101 | focused: true
102 | },
103 | () => {
104 | /**
105 | * Attach a native event listener to the document. We have to use `focusin` since
106 | * native `focus` event doesn't bubble. See
107 | * https://developer.mozilla.org/en-US/docs/Web/Events/focusin and
108 | * https://developer.mozilla.org/en-US/docs/Web/Events/focus
109 | */
110 | document.addEventListener('focusin', this._onFocusIn)
111 | onFocus(evt)
112 | }
113 | )
114 | }
115 | }
116 |
117 | /**
118 | * @private
119 | * @method onBlur
120 | */
121 | onBlur = evt => {
122 | evt.persist() // Persist the original event since it will be fired later
123 | this._lastBlurEvent = evt
124 | }
125 |
126 | /**
127 | * Checks if the parentNode contains the node
128 | *
129 | * @private
130 | * @method isInsideNode
131 | * @param parentNode
132 | * @param node
133 | * @returns {boolean}
134 | */
135 | isInsideNode = (parentNode, node) => {
136 | if (process.env.NODE_ENV === 'development') {
137 | if (parentNode == null || Object(parentNode).nodeType !== 1) {
138 | throw new Error(
139 | 'A ref to a valid DOM Node must be supplied to' +
140 | ' FocusWithin.\n' +
141 | ' You have probably provided a ref to a React Element.\n See https://reactjs.org/docs/react-api.html#refs'
142 | )
143 | }
144 | }
145 | // return false in case parentNode does not exist
146 | if (parentNode == null) {
147 | return false
148 | }
149 | return parentNode.contains(node)
150 | }
151 |
152 | render() {
153 | const { children } = this.props
154 | const { focused } = this.state
155 |
156 | const events = {
157 | onFocus: this.onFocus,
158 | onBlur: this.onBlur
159 | }
160 |
161 | if (typeof children === 'function') {
162 | return React.cloneElement(
163 | children({
164 | focused,
165 | getRef: this.ref
166 | }),
167 | events
168 | )
169 | }
170 |
171 | return (
172 |
173 | {children}
174 |
175 | )
176 | }
177 | }
178 |
179 | FocusWithin.propTypes = {
180 | /**
181 | * Function has the following signature:
182 | * `({ focused: Boolean, getRef: Function }) => React.Element`
183 | */
184 | children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired,
185 | onBlur: PropTypes.func,
186 | onFocus: PropTypes.func
187 | }
188 |
189 | FocusWithin.defaultProps = {
190 | onBlur: () => {},
191 | onFocus: () => {}
192 | }
193 |
194 | export default FocusWithin
195 |
--------------------------------------------------------------------------------
/packages/react-focus-within/src/index.js:
--------------------------------------------------------------------------------
1 | export { default as FocusWithin } from './FocusWithin'
2 |
--------------------------------------------------------------------------------
/packages/with-selector/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # @component-driven/with-selector
2 |
3 | ## 2.2.0
4 |
5 | ### Minor Changes
6 |
7 | - 3b955cf: Add support for nested selectors
8 |
9 | ## 2.1.1
10 |
11 | ### Patch Changes
12 |
13 | - 43e732f: Bump prismjs from 1.20.0 to 1.21.0
14 | - b527d04: Bump lodash from 4.17.15 to 4.17.19
15 | - eaea858: Bump elliptic from 6.5.2 to 6.5.3
16 | - 7952d76: Bump markdown-to-jsx from 6.11.1 to 6.11.4
17 | - 5e71065: Bump http-proxy from 1.18.0 to 1.18.1
18 |
19 | ## 2.1.0
20 |
21 | ### Minor Changes
22 |
23 | - 58f8ad4: Make it possible to use complex selectors like `:active:not([aria-disabled="true"])`
24 |
25 | ## 2.0.0
26 |
27 | ### Major Changes
28 |
29 | - a2dbfb1: Switch to a monorepo setup and use [Changesets](https://github.com/atlassian/changesets).
30 |
31 | Each package is a separate npm package.
32 |
--------------------------------------------------------------------------------
/packages/with-selector/Readme.md:
--------------------------------------------------------------------------------
1 | Allows previewing a wrapped component in a specific pseudo-state like hover, focused, active.
2 |
3 | Please note, that it required the pseudo-styles to be present on your component. Doesn't work with default HTML elements without pseudo-styles overrides.
4 |
5 | ```jsx
6 | import styled from 'styled-components'
7 |
8 | const Button = styled('button')`
9 | padding: 10px;
10 | margin-right: 5px;
11 | border: 2px solid blue;
12 | border-radius: 4px;
13 | background: #efefef;
14 |
15 | &:hover:enabled {
16 | background: #ccc;
17 | border-color: red;
18 | }
19 |
20 | &:focus {
21 | outline: none;
22 | border-color: orange;
23 | }
24 |
25 | &:active {
26 | border-color: #333;
27 | background: #888;
28 | color: #fff;
29 |
30 | &:not([aria-disabled='true']) {
31 | background: cadetblue;
32 | border-color: darkblue;
33 | color: #f5f5f5;
34 | }
35 | }
36 |
37 | &.custom-class {
38 | background: green;
39 | }
40 | `
41 |
42 | ;<>
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 | >
60 | ```
61 |
62 | ## Support for nested selectors
63 |
64 | ```jsx
65 | import styled from 'styled-components'
66 |
67 | const InputWrapper = styled('div')({
68 | padding: 4,
69 | border: '1px solid #ccc',
70 | borderRadius: 3,
71 | background: '#efefef',
72 | '& > input': {
73 | border: '1px solid green'
74 | },
75 | '> button': {
76 | position: 'relative',
77 | appearance: 'none',
78 | '::after': {
79 | position: 'absolute',
80 | right: 0,
81 | top: 0,
82 | background: 'pink',
83 | content: "''"
84 | }
85 | },
86 | ':focus-within': {
87 | background: '#ccc',
88 | input: {
89 | outline: 'none',
90 | borderColor: 'red'
91 | },
92 | button: {
93 | marginLeft: 10,
94 | borderColor: 'yellow',
95 | '::after': {
96 | content: "'focus'"
97 | }
98 | }
99 | }
100 | })
101 |
102 | const Input = React.forwardRef((props, ref) => (
103 |
104 |
105 |
106 |
107 | ))
108 |
109 | ;<>
110 |
111 |
112 |
113 |
114 | >
115 | ```
116 |
--------------------------------------------------------------------------------
/packages/with-selector/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@component-driven/with-selector",
3 | "description": "Preview components in specific pseudo-state like :hover, :focused, :active.",
4 | "version": "2.2.0",
5 | "main": "dist/with-selector.cjs.js",
6 | "module": "dist/with-selector.esm.js",
7 | "peerDependencies": {
8 | "react": ">=16.0",
9 | "react-dom": ">=16.0"
10 | },
11 | "author": "Andrey Okonetchnikov ",
12 | "license": "MIT",
13 | "publishConfig": {
14 | "access": "public"
15 | },
16 | "dependencies": {
17 | "nanoid": "^3.1.7"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/packages/with-selector/src/WithSelector.js:
--------------------------------------------------------------------------------
1 | import { cloneElement, useEffect, useRef, useState } from 'react'
2 | import { customAlphabet } from 'nanoid'
3 |
4 | function addStylesheetRule(rule) {
5 | const styleEl = document.createElement('style')
6 | document.head.appendChild(styleEl)
7 | const styleSheet = styleEl.sheet
8 | styleSheet.insertRule(rule, styleSheet.cssRules.length)
9 | }
10 |
11 | const generateCssClassName = customAlphabet(
12 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
13 | 16
14 | )
15 |
16 | // Inspired by https://codesandbox.io/s/pseudo-class-sticker-sheet-jiu2x
17 | const useAddSelector = (ref, selector) => {
18 | const [modifiedClassName, setModifiedClassName] = useState('')
19 | useEffect(() => {
20 | const className = ref.current?.classList[ref.current.classList.length - 1]
21 | const fullSelector = `${className && `.${className}`}${selector}`
22 | const newClassName = generateCssClassName()
23 | let newRules = []
24 | for (const styleSheet of document.styleSheets) {
25 | for (const rule of styleSheet.cssRules) {
26 | if (rule.selectorText?.startsWith(fullSelector)) {
27 | /**
28 | * Replace current CSS selector with the generated one so that
29 | * after adding the newClassName all children can be matched
30 | * i.e. we map:
31 | * .component:focus > input -> .generatedClass > input
32 | */
33 | const CSSSelector = rule.selectorText.replace(fullSelector, `.${newClassName}`)
34 | newRules.push(`${CSSSelector} { ${rule.style.cssText} }`)
35 | }
36 | }
37 | if (newRules.length > 0) {
38 | newRules.forEach(addStylesheetRule)
39 | setModifiedClassName(newClassName)
40 | break // Avoid triggering infinite loop since we're modifying stylesheets
41 | }
42 | }
43 | }, [ref, selector])
44 | return [modifiedClassName]
45 | }
46 |
47 | const WithSelector = (props) => {
48 | const ref = useRef(null)
49 | const [modifiedClassName] = useAddSelector(ref, props.selector)
50 |
51 | return cloneElement(props.children, {
52 | ref: ref,
53 | className: modifiedClassName
54 | })
55 | }
56 |
57 | export default WithSelector
58 |
--------------------------------------------------------------------------------
/packages/with-selector/src/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './WithSelector'
2 |
--------------------------------------------------------------------------------
/styleguide.config.js:
--------------------------------------------------------------------------------
1 | const path = require("path")
2 | const findUp = require("find-up")
3 |
4 | const packages = [
5 | "components",
6 | "react-design-tokens",
7 | "react-focus-within",
8 | "with-selector",
9 | "mixins"
10 | ]
11 |
12 | const file = (filepath) => path.join(__dirname, filepath)
13 |
14 | module.exports = {
15 | sections: [
16 | {
17 | name: "Introduction",
18 | content: "./Readme.md"
19 | },
20 | {
21 | name: "Packages",
22 | sections: packages.map((pkg) => ({
23 | name: pkg,
24 | components: `packages/${pkg}/**/[A-Z]*.{js,ts,jsx,tsx}`
25 | })),
26 | sectionDepth: 1
27 | }
28 | ],
29 | styleguideComponents: {
30 | Wrapper: file(".styleguidist/StyleguideProvider.tsx")
31 | },
32 | require: [file(".styleguidist/setup.js")],
33 | propsParser: require("react-docgen-typescript").withCustomConfig(file("tsconfig.json"), {
34 | propFilter(prop) {
35 | if (prop.parent) {
36 | return (
37 | !prop.parent.fileName.includes("node_modules") ||
38 | prop.parent.fileName.includes("@types/styled-system")
39 | )
40 | }
41 | return true
42 | },
43 | componentNameResolver: (exp, source) =>
44 | exp.getName() === "StyledComponentClass" &&
45 | require("react-docgen-typescript").getDefaultExportForFile(source)
46 | }).parse,
47 | webpackConfig: {
48 | module: {
49 | rules: [
50 | {
51 | test: /\.(jsx?|tsx?)$/,
52 | exclude: /node_modules/,
53 | loader: "babel-loader"
54 | }
55 | ]
56 | }
57 | },
58 | getExampleFilename(componentPath) {
59 | // Try to locate ${componentName}.md in the same directory
60 | // and if not found fallback to Readme.md
61 | const componentName = path.basename(componentPath, path.extname(componentPath))
62 | const examplePath = findUp.sync(`${componentName}.md`, { cwd: componentPath })
63 | if (examplePath) {
64 | return examplePath
65 | }
66 | const readmePath = findUp.sync("Readme.md", { cwd: componentPath })
67 | console.log(readmePath)
68 | if (readmePath) {
69 | return readmePath
70 | }
71 | console.error(`Could not find example file for ${componentPath}`)
72 | },
73 | getComponentPathLine(componentPath) {
74 | const componentName = path.basename(componentPath, path.extname(componentPath))
75 | const pkgPath = findUp.sync("package.json", { cwd: componentPath })
76 | if (!pkgPath) {
77 | console.error(`Could not find \`package.json\` for ${componentName}`)
78 | return ""
79 | }
80 | const { name } = require(pkgPath)
81 | return `import { ${componentName} } from "${name}"`
82 | },
83 | exampleMode: "expand",
84 | usageMode: "collapse",
85 | showSidebar: true,
86 | pagePerSection: true,
87 | skipComponentsWithoutExample: true,
88 | ribbon: {
89 | // Link to open on the ribbon click (required)
90 | url: "https://github.com/component-driven/ui",
91 | // Text to show on the ribbon (optional)
92 | text: "Fork me on GitHub"
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowJs": true,
4 | "alwaysStrict": true,
5 | "esModuleInterop": true,
6 | "forceConsistentCasingInFileNames": true,
7 | "isolatedModules": true,
8 | "jsx": "preserve",
9 | "lib": ["dom", "es2017"],
10 | "module": "esnext",
11 | "moduleResolution": "node",
12 | "noEmit": true,
13 | "noFallthroughCasesInSwitch": true,
14 | "noUnusedLocals": true,
15 | "noUnusedParameters": true,
16 | "resolveJsonModule": true,
17 | "skipLibCheck": true,
18 | "strict": true,
19 | "target": "esnext"
20 | },
21 | "exclude": ["packages/**/node_modules"],
22 | "include": ["**/*.ts", "**/*.tsx"]
23 | }
24 |
--------------------------------------------------------------------------------