├── .circleci
└── config.yml
├── .github
├── CONTRIBUTING.md
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
└── PULL_REQUEST_TEMPLATE.md
├── .gitignore
├── .npmignore
├── .prettierrc
├── .storybook
├── .babelrc
├── addons.js
├── config.js
└── webpack.config.js
├── LICENSE
├── README.md
├── babel.config.js
├── cypress.ci.json
├── cypress.dev.json
├── cypress.json
├── cypress
├── plugins
│ └── index.js
└── support
│ ├── commands.js
│ ├── commands
│ └── assertAreas.js
│ └── index.js
├── examples
├── Square.jsx
├── all.test.js
├── components
│ ├── Box
│ │ ├── DisplayOverride.jsx
│ │ └── DisplayOverride.test.js
│ ├── Composition
│ │ ├── declaration
│ │ │ ├── GridTemplate.jsx
│ │ │ ├── GridTemplate.test.js
│ │ │ ├── OrderProp.jsx
│ │ │ ├── OrderProp.test.js
│ │ │ ├── TemplateIndentation.jsx
│ │ │ ├── TemplateIndentation.test.js
│ │ │ ├── TemplatePeriod.jsx
│ │ │ ├── TemplatePeriod.test.js
│ │ │ ├── Templateless.jsx
│ │ │ └── Templateless.test.js
│ │ └── rendering
│ │ │ ├── NamespaceCollision.jsx
│ │ │ ├── NamespaceCollision.test.js
│ │ │ ├── NestedComposition.jsx
│ │ │ ├── NestedComposition.test.js
│ │ │ ├── WeakArea.jsx
│ │ │ ├── WeakAreas.test.js
│ │ │ ├── behaviors
│ │ │ ├── Bell.jsx
│ │ │ ├── Bell.test.js
│ │ │ ├── MobileFirst.jsx
│ │ │ ├── MobileFirst.test.js
│ │ │ ├── Notch.jsx
│ │ │ └── Notch.test.js
│ │ │ └── responsive-props
│ │ │ ├── BreakpointEdges.jsx
│ │ │ ├── BreakpointEdges.test.js
│ │ │ ├── BreakpointSpecific.jsx
│ │ │ ├── BreakpointSpecific.test.js
│ │ │ ├── InclusiveNotch.jsx
│ │ │ ├── InclusiveNotch.test.js
│ │ │ ├── MobileFirst.jsx
│ │ │ └── MobileFirst.test.js
│ ├── Only
│ │ ├── OnlyCustomBreakpoints.jsx
│ │ ├── OnlyCustomBreakpoints.test.js
│ │ ├── OnlyDefaultBehavior.jsx
│ │ └── OnlyDefaultBehavior.test.js
│ └── Visible
│ │ ├── Visible.test.js
│ │ └── VisibleDefaultBehavior.jsx
├── configuration
│ ├── CustomBreakpoints.jsx
│ ├── CustomBreakpoints.test.js
│ ├── CustomUnit.jsx
│ └── CustomUnit.test.js
├── hooks
│ ├── UseBreakpointChange.jsx
│ ├── UseResponsiveProps.jsx
│ ├── UseResponsiveValue.jsx
│ ├── UseViewportChange.jsx
│ ├── useBreakpointChange.test.js
│ ├── useResponsiveProps.test.js
│ ├── useResponsiveValue.test.js
│ └── useViewportChange.test.js
├── index.js
├── recipes
│ ├── IterativeAreas.jsx
│ └── IterativeAreas.test.js
├── regression
│ ├── DisplayNames.jsx
│ ├── DisplayNames.test.js
│ ├── OnlyUnmount.jsx
│ ├── OnlyUnmount.test.js
│ ├── ParentRerendering.jsx
│ ├── ParentRerendering.test.js
│ ├── SingleResponsiveProp.jsx
│ ├── SingleResponsiveProp.test.js
│ ├── StylesUndefined.jsx
│ └── StylesUndefined.test.js
├── semantics
│ ├── PolymorphicProp.jsx
│ └── PolymorphicProp.test.js
├── styles.css
└── utils
│ ├── MakeResponsive
│ ├── MakeResponsive.jsx
│ └── makeResponsive.test.js
│ └── Query
│ ├── Query.jsx
│ └── query.test.js
├── lerna.json
├── logo-full.png
├── logo.svg
├── materials
├── example-thumbnails
│ ├── basic-composition.jpg
│ ├── codesandbox
│ │ ├── thumbnail-blue.jpg
│ │ ├── thumbnail-violet.jpg
│ │ └── thumbnail.jpg
│ ├── conditional-rendering.jpg
│ ├── custom-configuration.jpg
│ ├── nested-composition.jpg
│ ├── responsive-props.jpg
│ └── shorthand-media-query.jpg
├── github
│ ├── contrib-e2e-test.png
│ ├── contrib-new-story.png
│ ├── contrib-unit-test.png
│ └── gitub-preview.jpg
├── react-finland-thumbnail.jpg
└── react-vienna-thumbnail.jpg
├── package.json
├── packages
├── atomic-layout-core
│ ├── LICENSE
│ ├── README.md
│ ├── global.d.ts
│ ├── jest.config.js
│ ├── jest.setup.js
│ ├── package.json
│ ├── rollup.config.js
│ ├── src
│ │ ├── Layout.spec.ts
│ │ ├── Layout.ts
│ │ ├── const
│ │ │ ├── defaultOptions.ts
│ │ │ ├── propAliases.ts
│ │ │ └── props.ts
│ │ ├── index.ts
│ │ └── utils
│ │ │ ├── breakpoints
│ │ │ ├── closeBreakpoint
│ │ │ │ ├── closeBreakpoint.spec.ts
│ │ │ │ ├── closeBreakpoint.ts
│ │ │ │ └── index.ts
│ │ │ ├── getAreaRecords
│ │ │ │ ├── behaviorUp.spec.ts
│ │ │ │ ├── getAreaRecords.spec.ts
│ │ │ │ ├── getAreaRecords.ts
│ │ │ │ └── index.ts
│ │ │ ├── mergeAreaRecords
│ │ │ │ ├── index.ts
│ │ │ │ ├── mergeAreaRecords.spec.ts
│ │ │ │ └── mergeAreaRecords.ts
│ │ │ ├── openBreakpoint
│ │ │ │ ├── index.ts
│ │ │ │ ├── openBreakpoint.spec.ts
│ │ │ │ └── openBreakpoint.ts
│ │ │ ├── shouldMergeBreakpoints
│ │ │ │ ├── index.ts
│ │ │ │ ├── shouldMergeBreakpoints.spec.ts
│ │ │ │ └── shouldMergeBreakpoints.ts
│ │ │ └── withBreakpoints
│ │ │ │ ├── index.ts
│ │ │ │ ├── matchMedia.mock.ts
│ │ │ │ ├── withBreakpoints.spec.ts
│ │ │ │ └── withBreakpoints.ts
│ │ │ ├── functions
│ │ │ ├── compose
│ │ │ │ ├── compose.spec.ts
│ │ │ │ ├── compose.ts
│ │ │ │ └── index.ts
│ │ │ ├── invariant
│ │ │ │ ├── index.ts
│ │ │ │ ├── invariant.spec.ts
│ │ │ │ └── invariant.ts
│ │ │ ├── isset
│ │ │ │ ├── index.ts
│ │ │ │ ├── isset.spec.ts
│ │ │ │ └── isset.ts
│ │ │ ├── memoizeWith
│ │ │ │ ├── index.ts
│ │ │ │ ├── memoizeWith.spec.ts
│ │ │ │ └── memoizeWith.ts
│ │ │ ├── pop
│ │ │ │ ├── index.ts
│ │ │ │ ├── pop.spec.ts
│ │ │ │ └── pop.ts
│ │ │ ├── throttle
│ │ │ │ ├── index.ts
│ │ │ │ ├── throttle.spec.ts
│ │ │ │ └── throttle.ts
│ │ │ └── warn
│ │ │ │ ├── index.ts
│ │ │ │ ├── warn.spec.ts
│ │ │ │ └── warn.ts
│ │ │ ├── math
│ │ │ ├── transformNumeric.spec.ts
│ │ │ └── transformNumeric.ts
│ │ │ ├── strings
│ │ │ ├── capitalize
│ │ │ │ ├── capitalize.spec.ts
│ │ │ │ ├── capitalize.ts
│ │ │ │ └── index.ts
│ │ │ ├── getPrefix
│ │ │ │ ├── getPrefix.spec.ts
│ │ │ │ ├── getPrefix.ts
│ │ │ │ └── index.ts
│ │ │ ├── hashString
│ │ │ │ ├── hashString.spec.ts
│ │ │ │ ├── hashString.ts
│ │ │ │ └── index.ts
│ │ │ ├── isAreaName
│ │ │ │ ├── index.ts
│ │ │ │ ├── isAreaName.spec.ts
│ │ │ │ └── isAreaName.ts
│ │ │ ├── parsePropName
│ │ │ │ ├── index.ts
│ │ │ │ ├── parsePropName.spec.ts
│ │ │ │ └── parsePropName.ts
│ │ │ ├── sanitizeTemplateArea
│ │ │ │ ├── index.ts
│ │ │ │ ├── sanitizeTemplateArea.spec.ts
│ │ │ │ └── sanitizeTemplateArea.ts
│ │ │ ├── sanitizeTemplateString
│ │ │ │ ├── index.ts
│ │ │ │ ├── sanitizeTemplateString.spec.ts
│ │ │ │ └── sanitizeTemplateString.ts
│ │ │ ├── toDashedString
│ │ │ │ ├── index.ts
│ │ │ │ ├── toDashedString.spec.ts
│ │ │ │ └── toDashedString.ts
│ │ │ └── toLowerCaseFirst
│ │ │ │ ├── index.ts
│ │ │ │ ├── toLowerCaseFirst.spec.ts
│ │ │ │ └── toLowerCaseFirst.ts
│ │ │ ├── styles
│ │ │ ├── applyStyles
│ │ │ │ ├── applyStyles.ts
│ │ │ │ └── index.ts
│ │ │ ├── createMediaQuery
│ │ │ │ ├── createMediaQuery.spec.ts
│ │ │ │ ├── createMediaQuery.ts
│ │ │ │ └── index.ts
│ │ │ └── normalizeQuery
│ │ │ │ ├── index.ts
│ │ │ │ ├── normalizeQuery.spec.ts
│ │ │ │ └── normalizeQuery.ts
│ │ │ └── templates
│ │ │ ├── generateComponents
│ │ │ ├── generateComponents.ts
│ │ │ └── index.ts
│ │ │ ├── getAreasList
│ │ │ ├── getAreasList.spec.ts
│ │ │ ├── getAreasList.ts
│ │ │ └── index.ts
│ │ │ └── parseTemplates
│ │ │ ├── filterTemplateProps.spec.ts
│ │ │ ├── filterTemplateProps.ts
│ │ │ ├── index.ts
│ │ │ └── parseTemplates.ts
│ └── tsconfig.json
├── atomic-layout-emotion
│ ├── LICENSE
│ ├── README.md
│ ├── babel.config.js
│ ├── jest.config.js
│ ├── logo-full.png
│ ├── package.json
│ ├── rollup.config.js
│ ├── src
│ │ └── index.ts
│ ├── test
│ │ ├── refs.spec.tsx
│ │ └── ssr.spec.tsx
│ ├── tsconfig.json
│ └── tslint.json
└── atomic-layout
│ ├── LICENSE
│ ├── README.md
│ ├── babel.config.js
│ ├── jest.config.js
│ ├── logo-full.png
│ ├── package.json
│ ├── rollup.config.js
│ ├── src
│ ├── components
│ │ ├── Box.spec.tsx
│ │ ├── Box.tsx
│ │ ├── Composition.tsx
│ │ ├── Only.tsx
│ │ └── Visible.tsx
│ ├── hooks
│ │ ├── useBreakpointChange.ts
│ │ ├── useMediaQuery.ts
│ │ ├── useResponsiveProps.ts
│ │ ├── useResponsiveQuery.ts
│ │ ├── useResponsiveValue.ts
│ │ └── useViewportChange.ts
│ ├── index.ts
│ └── utils
│ │ ├── MakeResponsive.spec.tsx
│ │ ├── forwardRef.ts
│ │ ├── getBreakpointsByQuery.test.ts
│ │ ├── getBreakpointsByQuery.ts
│ │ ├── makeResponsive.tsx
│ │ ├── query.test.ts
│ │ ├── query.ts
│ │ └── withPlaceholder.tsx
│ ├── test
│ ├── createForwardRefTest.tsx
│ ├── createSsrTest.tsx
│ ├── propAliases.spec.tsx
│ ├── refs.spec.tsx
│ └── ssr.spec.tsx
│ ├── tsconfig.json
│ └── tslint.json
├── tslint.json
└── yarn.lock
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # Supported ways to fund Atomic layout efforts
2 | open_collective: atomic-layout
3 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | ---
5 |
6 | ## When:
7 |
8 |
9 |
10 | 1. Declare the following Composition: ...
11 | 2. And change the breakpoint to ...
12 |
13 | ## Current behavior:
14 |
15 |
16 |
17 | ## Expected behavior:
18 |
19 |
20 |
21 | ## Environment:
22 |
23 | - node vesrion: `0.0.0`
24 | - npm version: `0.0.0`
25 | - atomic-layout version: `0.0.0`
26 |
27 | ## Screenshots:
28 |
29 |
30 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | ---
5 |
6 | ## What:
7 |
8 | Please describe your change request suggestion.
9 |
10 | ## Why:
11 |
12 | What is the motivation for this feature/change?
13 |
14 | ## How:
15 |
16 | What is the steps needed for this feature/change? Feel free to be descriptive in technical details here.
17 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ## Changes
2 |
3 |
4 |
5 | - Changes A
6 |
7 | ## Issues
8 |
9 |
10 |
11 | - Closes #ISSUE_NUMBER
12 |
13 | ## Release version
14 |
15 |
16 |
17 | - [ ] internal (no release required)
18 | - [ ] patch (bug fixes)
19 | - [ ] minor (backward-compatible changes)
20 | - [ ] major (backward-incompatible, breaking changes)
21 |
22 | ## Contributor's checklist
23 |
24 |
25 |
26 | - [ ] My branch is up-to-date with the latest `master`
27 |
28 |
34 |
35 | - [ ] I ran `yarn verify` to see the build and tests pass
36 |
37 |
40 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Internal
2 | __*
3 | internal
4 | deprecated
5 | yarn-*
6 |
7 | # Modules
8 | node_modules
9 | /coverage
10 |
11 | # Build
12 | lib
13 |
14 | # Cypress
15 | cypress/fixtures
16 | cypress/screenshots
17 | cypress/videos
18 | packages/**/cypress
19 | storybook-static
20 |
21 | # Editors
22 | .idea
23 | .vscode
24 |
25 | # OS
26 | .DS_*
27 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | # Configurations
2 | .github
3 | .circleci
4 | .storybook
5 | .vscode
6 | .babelrc
7 | *.config.js
8 | .gitignore
9 | .prettierrc
10 | tsconfig.*
11 | tslint.*
12 | jest.*
13 | webpack.*
14 | yarn-*
15 |
16 | # Sources
17 | __*
18 | src
19 | internal
20 | examples
21 | stories
22 | examples
23 | /*.png
24 | materials
25 | storybook-static
26 |
27 | # Tests
28 | cypress*
29 | coverage
30 | jest.*
31 | tests
32 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": false,
3 | "trailingComma": "all",
4 | "jsxBracketSameLine": false,
5 | "singleQuote": true,
6 | "useTabs": false,
7 | "tabWidth": 2,
8 | "arrowParens": "always"
9 | }
10 |
--------------------------------------------------------------------------------
/.storybook/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../babel.config.js"
3 | }
4 |
--------------------------------------------------------------------------------
/.storybook/addons.js:
--------------------------------------------------------------------------------
1 | import '@storybook/addon-actions/register'
2 |
--------------------------------------------------------------------------------
/.storybook/config.js:
--------------------------------------------------------------------------------
1 | import { addParameters, configure } from '@storybook/react'
2 |
3 | addParameters({
4 | options: {
5 | brandName: 'Atomic Layout',
6 | brandUrl: 'https://github.com/kettanaito/atomic-layout',
7 | },
8 | })
9 |
10 | configure(() => require('../examples'), module)
11 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Artem Zakharchenko
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | [
4 | '@babel/preset-env',
5 | {
6 | modules: false,
7 | },
8 | ],
9 | '@babel/preset-react',
10 | '@babel/preset-typescript',
11 | ],
12 | plugins: [
13 | '@babel/plugin-proposal-class-properties',
14 | '@babel/plugin-proposal-object-rest-spread',
15 | '@babel/plugin-proposal-export-default-from',
16 | '@babel/plugin-proposal-export-namespace-from',
17 | ],
18 | env: {
19 | test: {
20 | presets: ['@babel/preset-env', '@babel/preset-react'],
21 | },
22 | },
23 | }
24 |
--------------------------------------------------------------------------------
/cypress.ci.json:
--------------------------------------------------------------------------------
1 | {
2 | "fileServerFolder": "{PACKAGE}/storybook-static"
3 | }
4 |
--------------------------------------------------------------------------------
/cypress.dev.json:
--------------------------------------------------------------------------------
1 | {
2 | "baseUrl": "http://localhost:6020"
3 | }
4 |
--------------------------------------------------------------------------------
/cypress.json:
--------------------------------------------------------------------------------
1 | {
2 | "integrationFolder": "./examples",
3 | "testFiles": "**/*.test.js",
4 | "viewportWidth": 500
5 | }
6 |
--------------------------------------------------------------------------------
/cypress/plugins/index.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs')
2 | const webpackPreprocessor = require('@cypress/webpack-preprocessor')
3 | const babelConfig = require('../../babel.config')
4 |
5 | const webpackOptions = {
6 | module: {
7 | rules: [
8 | {
9 | test: /\.(j|t)sx?$/,
10 | exclude: /node_modules/,
11 | use: [
12 | {
13 | loader: 'babel-loader',
14 | options: babelConfig,
15 | },
16 | ],
17 | },
18 | ],
19 | },
20 | resolve: {
21 | extensions: ['.ts', '.tsx', '.spec.jsx', '.spec.js', '.jsx', '.js'],
22 | },
23 | }
24 |
25 | const getCypressConfig = (env) => {
26 | const { envName = '', package } = env
27 | const configFilename = ['cypress', envName, 'json'].filter(Boolean).join('.')
28 |
29 | const baseConfig = require('../../cypress.json')
30 | const envConfig = envName ? require(`../../cypress.${envName}.json`) : {}
31 |
32 | if (envName) {
33 | console.log(`Extending base Cypress config with "${configFilename}"`)
34 | }
35 |
36 | const resolvedConfig = Object.assign({}, baseConfig, envConfig)
37 |
38 | if (
39 | resolvedConfig.fileServerFolder &&
40 | resolvedConfig.fileServerFolder.includes('{PACKAGE}')
41 | ) {
42 | if (package == null) {
43 | throw new Error(
44 | `Failed to run Cypress against a static file server with no "package" environmental variable specified (${package}).`,
45 | )
46 | }
47 |
48 | const nestedServerFolder = resolvedConfig.fileServerFolder.replace(
49 | '{PACKAGE}',
50 | package,
51 | )
52 | resolvedConfig.fileServerFolder = nestedServerFolder
53 |
54 | console.log(`Loading test suites at "${nestedServerFolder}"`)
55 | }
56 |
57 | console.log('Resolved Cypress config:')
58 | console.log(JSON.stringify(resolvedConfig, null, 2))
59 |
60 | return resolvedConfig
61 | }
62 |
63 | module.exports = (on, config) => {
64 | on(
65 | 'file:preprocessor',
66 | webpackPreprocessor({
67 | webpackOptions,
68 | watchOptions: {},
69 | }),
70 | )
71 |
72 | return getCypressConfig(config.env)
73 | }
74 |
--------------------------------------------------------------------------------
/cypress/support/commands/assertAreas.js:
--------------------------------------------------------------------------------
1 | const assertAxis = (areas, areaName, getArea, createSelector, areaPosition) => {
2 | const [rowIndex, columnIndex] = areaPosition
3 |
4 | const existingAreasOtherThan = (areaName) => (siblingAreaName) => {
5 | return !!siblingAreaName && siblingAreaName !== areaName
6 | }
7 |
8 | // Assert same Y axis with the areas in the same row
9 | const rowSiblings = areas[rowIndex].filter(existingAreasOtherThan(areaName))
10 |
11 | rowSiblings.map((siblingAreaName) => {
12 | getArea(areaName)
13 | .haveSameAxis('y', createSelector(siblingAreaName))
14 | .notIntersectWith(createSelector(siblingAreaName))
15 | })
16 |
17 | // Assert same X axis with the areas in the same column
18 | const columnSiblings = areas
19 | .reduce((acc, row) => {
20 | return acc.concat(row[columnIndex])
21 | }, [])
22 | .filter(existingAreasOtherThan(areaName))
23 |
24 | columnSiblings.forEach((siblingAreaName) => {
25 | getArea(areaName)
26 | .haveSameAxis('x', createSelector(siblingAreaName))
27 | .notIntersectWith(createSelector(siblingAreaName))
28 | })
29 | }
30 |
31 | export default function assertAreas(areas, context = null) {
32 | const createSelector = (areaName) => {
33 | return [context, `[data-area="${areaName}"]`].filter(Boolean).join(' ')
34 | }
35 | const getArea = (areaName) => cy.get(createSelector(areaName))
36 |
37 | areas.forEach((row, rowIndex) => {
38 | row.forEach((areaName, columnIndex) => {
39 | // Bypass "false" or missing areas
40 | if (!areaName) {
41 | return
42 | }
43 |
44 | // Assert that area is displayed on the page
45 | cy.log(`Assert area "${areaName}" is visible`)
46 | getArea(areaName).should('be.visible')
47 |
48 | // Assert that area has proper "grid-area" CSS property
49 | cy.log(`Assert area "${areaName}" has "grid-area:${areaName}"`)
50 | getArea(areaName).haveArea(areaName)
51 |
52 | // Assert area's axis relation and intersection with
53 | // the siblings in the same row and column.
54 | assertAxis(areas, areaName, getArea, createSelector, [
55 | rowIndex,
56 | columnIndex,
57 | ])
58 | })
59 | })
60 | }
61 |
--------------------------------------------------------------------------------
/cypress/support/index.js:
--------------------------------------------------------------------------------
1 | import './commands'
2 |
--------------------------------------------------------------------------------
/examples/Square.jsx:
--------------------------------------------------------------------------------
1 | import styled from 'supported-styling-library'
2 |
3 | const Square = styled.span`
4 | box-sizing: border-box;
5 |
6 | display: flex;
7 | align-items: center;
8 | justify-content: center;
9 |
10 | min-height: 40px;
11 | height: ${({ height }) => height || '100%'};
12 | width: ${({ width }) => width || '100%'};
13 | background-color: hsla(235, 90%, 72%, 0.25);
14 | border: 1px solid hsla(235, 50%, 75%, 0.35);
15 | border-radius: 3px;
16 | color: hsla(235, 25%, 50%);
17 | font-size: 90%;
18 | font-weight: bold;
19 | text-transform: uppercase;
20 | `
21 |
22 | export default Square
23 |
--------------------------------------------------------------------------------
/examples/components/Box/DisplayOverride.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'supported-styling-library'
3 | import { Box } from 'atomic-layout'
4 |
5 | const StyledBox = styled.div`
6 | display: table;
7 | `
8 |
9 | const BoxDisplayOverride = () => {
10 | return (
11 |
12 | I should have display: table
13 |
14 | )
15 | }
16 |
17 | export default BoxDisplayOverride
18 |
--------------------------------------------------------------------------------
/examples/components/Box/DisplayOverride.test.js:
--------------------------------------------------------------------------------
1 | it('Supports CSS properties overrides', () => {
2 | cy.loadStory(['components', 'box'], ['properties-override'])
3 |
4 | cy.get('#box')
5 | .should('be.visible')
6 | .should('have.css', 'display', 'table')
7 | })
8 |
--------------------------------------------------------------------------------
/examples/components/Composition/declaration/GridTemplate.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Composition } from 'atomic-layout'
3 | import Square from '@stories/Square'
4 |
5 | const templateMobile = `
6 | header 100px
7 | content 1fr
8 | footer auto
9 | `
10 |
11 | const templateTablet = `
12 | header content footer 500px
13 | / 200px 1fr auto
14 | `
15 |
16 | const List = () => (
17 |
23 | {({ Header, Content, Footer }) => (
24 | <>
25 |
28 |
29 | Content
30 |
31 |
36 | >
37 | )}
38 |
39 | )
40 |
41 | export default List
42 |
--------------------------------------------------------------------------------
/examples/components/Composition/declaration/GridTemplate.test.js:
--------------------------------------------------------------------------------
1 | const { expect } = require('chai')
2 |
3 | describe('Grid template', function() {
4 | before(() => {
5 | cy.loadStory(
6 | ['components', 'composition', 'declaration'],
7 | ['grid-template-syntax'],
8 | )
9 | })
10 |
11 | beforeEach(() => {
12 | cy.setBreakpoint('xs')
13 | })
14 |
15 | it('Areas should not intersect', () => {
16 | cy.get('#header').notIntersectWith('#content')
17 | cy.get('#content').notIntersectWith('#footer')
18 |
19 | cy.setBreakpoint('md').then(() => {
20 | cy.get('#header').notIntersectWith('#content')
21 | cy.get('#content').notIntersectWith('#footer')
22 | })
23 | })
24 |
25 | describe('row', () => {
26 | it('Supports "auto" as the row height', () => {
27 | cy.get('#footer').should('have.css', 'height', '70px')
28 | })
29 |
30 | it('Supports exact row height', () => {
31 | cy.get('#header').should('have.css', 'height', '100px')
32 |
33 | cy.setBreakpoint('md').then(() => {
34 | cy.get('#header').should('have.css', 'height', '500px')
35 | cy.get('#content').should('have.css', 'height', '500px')
36 | })
37 | })
38 |
39 | it('Supports dynamic row height', () => {
40 | cy.get('#content').should('have.css', 'height', '40px')
41 | })
42 | })
43 |
44 | describe('column', () => {
45 | it('Supports "auto" as a column width', () => {
46 | cy.setBreakpoint('md').then(() => {
47 | cy.get('#footer').should('have.css', 'width', '50px')
48 | })
49 | })
50 |
51 | it('Supports exact column width', () => {
52 | cy.setBreakpoint('md').then(() => {
53 | cy.get('#header').should('have.css', 'width', '200px')
54 | })
55 | })
56 |
57 | it('Supports dynamic column width', () => {
58 | cy.setBreakpoint('md').then(() => {
59 | cy.get('#composition').then(([parent]) => {
60 | const { clientWidth: parentWidth } = parent
61 | const { header, content, footer } = parent.children
62 | const { clientWidth: headerWidth } = header
63 | const { clientWidth: contentWidth } = content
64 | const { clientWidth: footerWidth } = footer
65 |
66 | expect(contentWidth).to.equal(
67 | parentWidth - headerWidth - footerWidth - 10 * 2,
68 | )
69 | })
70 | })
71 | })
72 | })
73 | })
74 |
--------------------------------------------------------------------------------
/examples/components/Composition/declaration/OrderProp.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Composition, Box } from 'atomic-layout'
3 | import Square from '@stories/Square'
4 |
5 | const OrderPropExample = () => {
6 | return (
7 | <>
8 |
Template-less composition
9 |
10 | Template-less Composition should respect a custom order
{' '}
11 | prop of its children.
12 |
13 |
14 |
15 | First
16 |
17 |
18 | Second
19 |
20 |
21 | Third
22 |
23 |
24 |
25 | Regular composition
26 |
27 | When given explicit areas
/template
prop,
28 | Composition should ignore any given order
prop on its
29 | children areas, and always render according to the template.
30 |
31 |
32 | {(Areas) => (
33 | <>
34 |
35 | Left
36 |
37 |
38 | Center
39 |
40 |
41 | Right
42 |
43 | >
44 | )}
45 |
46 | >
47 | )
48 | }
49 |
50 | export default OrderPropExample
51 |
--------------------------------------------------------------------------------
/examples/components/Composition/declaration/OrderProp.test.js:
--------------------------------------------------------------------------------
1 | const { expect } = require('chai')
2 |
3 | describe('Order prop', function() {
4 | before(() => {
5 | cy.loadStory(['components', 'composition', 'declaration'], ['order-prop'])
6 | })
7 |
8 | describe('Template-less composition', () => {
9 | it('Renders the child with negative "order" first', () => {
10 | cy.get('#box-third').then(([thirdElement]) => {
11 | cy.get('#box-first').then(([firstElement]) => {
12 | expect(thirdElement.offsetTop).to.be.lessThan(firstElement.offsetTop)
13 | })
14 | })
15 | })
16 |
17 | it('Renders other children as-is', () => {
18 | cy.get('#box-first').then(([firstElement]) => {
19 | cy.get('#box-second').then(([secondElement]) => {
20 | expect(firstElement.offsetTop).to.be.lessThan(secondElement.offsetTop)
21 | })
22 | })
23 | })
24 | })
25 |
26 | describe('Composition with a template', () => {
27 | it('Ignores the "order" prop on area component', () => {
28 | cy.get('[data-area="right"]').then(([rightElement]) => {
29 | cy.get('[data-area="left"]').then(([leftElement]) => {
30 | expect(rightElement.offsetLeft).to.be.greaterThan(
31 | leftElement.offsetLeft,
32 | )
33 | })
34 | })
35 | })
36 |
37 | it('Renders areas according to the template', () => {
38 | cy.get('#regular-composition').assertAreas([['left', 'center', 'right']])
39 | })
40 | })
41 | })
42 |
--------------------------------------------------------------------------------
/examples/components/Composition/declaration/TemplateIndentation.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Composition } from 'atomic-layout'
3 | import Square from '@stories/Square'
4 |
5 | const unindentedAreas = `
6 | first
7 | second
8 | `
9 |
10 | const terriblyIndentedAreas = `
11 | third
12 | fourth
13 | `
14 |
15 | const TemplateIndentation = () => (
16 | <>
17 |
18 | {({ First, Second }) => (
19 | <>
20 |
21 | First
22 |
23 |
24 | Second
25 |
26 | >
27 | )}
28 |
29 |
30 |
31 | {({ Third, Fourth }) => (
32 | <>
33 |
34 | Third
35 |
36 |
37 | Fourh
38 |
39 | >
40 | )}
41 |
42 | >
43 | )
44 |
45 | export default TemplateIndentation
46 |
--------------------------------------------------------------------------------
/examples/components/Composition/declaration/TemplateIndentation.test.js:
--------------------------------------------------------------------------------
1 | describe('Template string indentation', () => {
2 | before(() => {
3 | cy.loadStory(
4 | ['components', 'composition', 'declaration'],
5 | ['template-indentation'],
6 | )
7 | })
8 |
9 | it('Supports unindented template string', () => {
10 | cy.get('#first')
11 | .should('be.visible')
12 | .haveArea('first')
13 | .notIntersectWith('#second')
14 |
15 | cy.get('#second')
16 | .should('be.visible')
17 | .haveArea('second')
18 | })
19 |
20 | it('Supports mis-indented template string', () => {
21 | cy.get('#third')
22 | .should('be.visible')
23 | .haveArea('third')
24 | .notIntersectWith('#fourth')
25 |
26 | cy.get('#fourth')
27 | .should('be.visible')
28 | .haveArea('fourth')
29 | })
30 | })
31 |
--------------------------------------------------------------------------------
/examples/components/Composition/declaration/TemplatePeriod.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Composition } from 'atomic-layout'
3 | import Square from '@stories/Square'
4 |
5 | const areas = `
6 | left . right
7 | `
8 |
9 | const TemplatePeriod = () => (
10 |
11 | {({ Left, Right }) => (
12 | <>
13 |
14 | Left
15 |
16 |
17 | Right
18 |
19 | >
20 | )}
21 |
22 | )
23 |
24 | export default TemplatePeriod
25 |
--------------------------------------------------------------------------------
/examples/components/Composition/declaration/TemplatePeriod.test.js:
--------------------------------------------------------------------------------
1 | const { expect } = require('chai')
2 |
3 | describe('Template period', () => {
4 | before(() => {
5 | cy.loadStory(
6 | ['components', 'composition', 'declaration'],
7 | ['template-period'],
8 | )
9 | })
10 |
11 | it('Renders named areas according to the template string', () => {
12 | cy.get('#left')
13 | .should('be.visible')
14 | .haveArea('left')
15 | .notIntersectWith('#right')
16 | cy.get('#right')
17 | .should('be.visible')
18 | .haveArea('right')
19 | })
20 |
21 | it('Renders dot (".") placeholder where specified', () => {
22 | cy.get('#left').then((leftAreaWrapper) => {
23 | cy.get('#right').then((rightAreaWrapper) => {
24 | const [leftArea] = leftAreaWrapper
25 | const [rightArea] = rightAreaWrapper
26 |
27 | expect(rightArea.offsetLeft).to.be.gt(
28 | leftArea.clientLeft + leftArea.offsetWidth + 20,
29 | )
30 | })
31 | })
32 | })
33 | })
34 |
--------------------------------------------------------------------------------
/examples/components/Composition/declaration/Templateless.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Composition } from 'atomic-layout'
3 | import Square from '@stories/Square'
4 |
5 | const TemplatelessComposition = () => (
6 |
7 | <>
8 | First
9 | Second
10 | Third
11 | >
12 |
13 | )
14 |
15 | export default TemplatelessComposition
16 |
--------------------------------------------------------------------------------
/examples/components/Composition/declaration/Templateless.test.js:
--------------------------------------------------------------------------------
1 | describe('Template-less composition', () => {
2 | before(() => {
3 | cy.loadStory(
4 | ['components', 'composition', 'declaration'],
5 | ['template-less-composition'],
6 | )
7 | })
8 |
9 | it('Renders children without crashing', () => {
10 | cy.get('#first').should('have.css', 'width', '200px')
11 | cy.get('#second').should('have.css', 'width', '300px')
12 | cy.get('#third').should('have.css', 'width', '400px')
13 | })
14 | })
15 |
--------------------------------------------------------------------------------
/examples/components/Composition/rendering/NamespaceCollision.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Composition } from 'atomic-layout'
3 |
4 | const Logo = () => Logo
5 | const Menu = () => Menu
6 |
7 | const Namespaces = () => (
8 |
9 | {(Areas) => (
10 | <>
11 |
12 |
13 |
14 |
15 |
16 |
17 | >
18 | )}
19 |
20 | )
21 |
22 | export default Namespaces
23 |
--------------------------------------------------------------------------------
/examples/components/Composition/rendering/NamespaceCollision.test.js:
--------------------------------------------------------------------------------
1 | describe('Namespace collision', () => {
2 | before(() => {
3 | cy.loadStory(
4 | ['components', 'composition', 'rendering'],
5 | ['namespace-collision'],
6 | )
7 | })
8 |
9 | it('Renders children without crashing', () => {
10 | cy.get('#composition').assertAreas([['logo', 'menu']])
11 | })
12 |
13 | it('Renders custom components without namespace collision', () => {
14 | cy.get('[data-area="logo"]').should('have.text', 'Logo')
15 | cy.get('[data-area="menu"]').should('have.text', 'Menu')
16 | })
17 | })
18 |
--------------------------------------------------------------------------------
/examples/components/Composition/rendering/NestedComposition.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Bell from './behaviors/Bell'
3 | import Notch from './behaviors/Notch'
4 |
5 | const NestedComposition = () => (
6 |
7 |
8 |
9 | )
10 |
11 | export default NestedComposition
12 |
--------------------------------------------------------------------------------
/examples/components/Composition/rendering/NestedComposition.test.js:
--------------------------------------------------------------------------------
1 | describe('Nested composition', () => {
2 | before(() => {
3 | cy.loadStory(
4 | ['components', 'composition', 'rendering'],
5 | ['nested-composition'],
6 | )
7 | })
8 |
9 | it('Parent composition behaves as bell', () => {
10 | cy.get('#notch').assertNotch()
11 | })
12 |
13 | it('Child composite behaves as notch', () => {
14 | cy.get('#bell').assertBell()
15 | })
16 | })
17 |
--------------------------------------------------------------------------------
/examples/components/Composition/rendering/WeakArea.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Composition } from 'atomic-layout'
3 | import Square from '@stories/Square'
4 |
5 | const WeakAreaRendering = () => (
6 |
12 | {({ Left, Center, Right, Extra }) => (
13 | <>
14 |
15 | Left
16 |
17 | {/* Area present only in some definitions */}
18 |
19 | Center
20 |
21 |
22 | Right
23 |
24 | {/* Undefined area component */}
25 | Foo
26 | >
27 | )}
28 |
29 | )
30 |
31 | export default WeakAreaRendering
32 |
--------------------------------------------------------------------------------
/examples/components/Composition/rendering/WeakAreas.test.js:
--------------------------------------------------------------------------------
1 | describe('Weak areas', () => {
2 | before(() => {
3 | cy.loadStory(['components', 'composition', 'rendering'], ['weak-areas'])
4 | })
5 |
6 | describe('On extra-small screen', () => {
7 | before(() => {
8 | cy.setBreakpoint('sm')
9 | })
10 |
11 | it('Renders existing areas', () => {
12 | cy.get('#composition').assertAreas([['left', 'right']])
13 | })
14 |
15 | it('Does not render weak area', () => {
16 | cy.get('#center').should('not.exist')
17 | })
18 |
19 | it('Does not render undefined area', () => {
20 | cy.get('#extra').should('not.exist')
21 | })
22 | })
23 |
24 | describe('On medium screen', () => {
25 | before(() => {
26 | cy.setBreakpoint('md')
27 | })
28 |
29 | it('Renders all areas', () => {
30 | cy.get('#composition').assertAreas([['left', 'center', 'right']])
31 | })
32 |
33 | it('Does not render undefined area', () => {
34 | cy.get('#extra').should('not.exist')
35 | })
36 | })
37 | })
38 |
--------------------------------------------------------------------------------
/examples/components/Composition/rendering/behaviors/Bell.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Composition } from 'atomic-layout'
3 | import Square from '@stories/Square'
4 |
5 | const template = 'first second'
6 | const templateSm = 'first second third'
7 | const templateLg = 'first second'
8 |
9 | const BellBehavior = ({ children }) => (
10 |
17 | {({ First, Second, Third }) => (
18 | <>
19 |
20 | {children || 'First'}
21 |
22 |
23 | Second
24 |
25 |
26 | Third
27 |
28 | >
29 | )}
30 |
31 | )
32 |
33 | export default BellBehavior
34 |
--------------------------------------------------------------------------------
/examples/components/Composition/rendering/behaviors/Bell.test.js:
--------------------------------------------------------------------------------
1 | it('Bell', () => {
2 | cy.loadStory(
3 | ['components', 'composition', 'rendering', 'behaviors'],
4 | ['bell'],
5 | )
6 |
7 | cy.get('#bell').assertBell()
8 | })
9 |
--------------------------------------------------------------------------------
/examples/components/Composition/rendering/behaviors/MobileFirst.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Composition } from 'atomic-layout'
3 | import Square from '@stories/Square'
4 |
5 | const Scenario = () => (
6 |
7 | {({ Left, Center, Right }) => (
8 | <>
9 |
10 | Left
11 |
12 |
13 | Center
14 |
15 |
16 | Right
17 |
18 | >
19 | )}
20 |
21 | )
22 |
23 | export default Scenario
24 |
--------------------------------------------------------------------------------
/examples/components/Composition/rendering/behaviors/MobileFirst.test.js:
--------------------------------------------------------------------------------
1 | it('Mobile first', () => {
2 | cy.loadStory(
3 | ['components', 'composition', 'rendering', 'behaviors'],
4 | ['mobile-first'],
5 | )
6 |
7 | const assertInline = () => {
8 | cy.get('#composition').assertAreas([['left', 'center', 'right']])
9 | }
10 |
11 | assertInline()
12 | cy.setBreakpoint('sm').then(assertInline)
13 | cy.setBreakpoint('md').then(assertInline)
14 | cy.setBreakpoint('lg').then(assertInline)
15 | cy.setBreakpoint('xl').then(assertInline)
16 | })
17 |
--------------------------------------------------------------------------------
/examples/components/Composition/rendering/behaviors/Notch.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Composition } from 'atomic-layout'
3 | import Square from '@stories/Square'
4 |
5 | const template = 'left center right'
6 | const templateMd = 'left right'
7 | const templateLg = 'left center right'
8 |
9 | const Scenario = () => (
10 |
17 | {({ Left, Center, Right }) => (
18 | <>
19 |
20 | Left
21 |
22 |
23 | Center
24 |
25 |
26 | Right
27 |
28 | >
29 | )}
30 |
31 | )
32 |
33 | export default Scenario
34 |
--------------------------------------------------------------------------------
/examples/components/Composition/rendering/behaviors/Notch.test.js:
--------------------------------------------------------------------------------
1 | it('Notch', () => {
2 | cy.loadStory(
3 | ['components', 'composition', 'rendering', 'behaviors'],
4 | ['notch'],
5 | )
6 |
7 | cy.get('#notch').assertNotch()
8 | })
9 |
--------------------------------------------------------------------------------
/examples/components/Composition/rendering/responsive-props/BreakpointEdges.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Composition } from 'atomic-layout'
3 | import Square from '@stories/Square'
4 |
5 | const BreakpointEdges = () => (
6 |
15 | Left
16 | Right
17 |
18 | )
19 |
20 | export default BreakpointEdges
21 |
--------------------------------------------------------------------------------
/examples/components/Composition/rendering/responsive-props/BreakpointEdges.test.js:
--------------------------------------------------------------------------------
1 | describe('Breakpoint edges', () => {
2 | before(() => {
3 | cy.loadStory(
4 | ['components', 'composition', 'rendering', 'responsive-props'],
5 | ['breakpoint-edges'],
6 | )
7 | })
8 |
9 | const assertMaxWidth = (value) => {
10 | return cy.get('#composition').should('have.css', 'max-width', value)
11 | }
12 |
13 | it('Renders correctly on initial render', () => {
14 | assertMaxWidth('200px')
15 | })
16 |
17 | it('Renders correctly at a breakpoint', () => {
18 | cy.setBreakpoint('sm').then(() => {
19 | assertMaxWidth('400px')
20 | })
21 | cy.setBreakpoint('md').then(() => {
22 | assertMaxWidth('400px')
23 | })
24 | cy.setBreakpoint('lg').then(() => {
25 | assertMaxWidth('100%')
26 | })
27 | cy.setBreakpoint('xl').then(() => {
28 | assertMaxWidth('100%')
29 | })
30 | })
31 |
32 | it('Renders correctly within a breakpoint', () => {
33 | // xs
34 | cy.setBreakpoint({ minWidth: 550 }).then(() => {
35 | assertMaxWidth('200px')
36 | })
37 |
38 | //sm
39 | cy.setBreakpoint({ minWidth: 650 }).then(() => {
40 | assertMaxWidth('400px')
41 | })
42 |
43 | // md
44 | cy.setBreakpoint({ minWidth: 850 }).then(() => {
45 | assertMaxWidth('400px')
46 | })
47 |
48 | // lg
49 | cy.setBreakpoint({ minWidth: 1050 }).then(() => {
50 | assertMaxWidth('100%')
51 | })
52 |
53 | // xl
54 | cy.setBreakpoint({ minWidth: 1400 }).then(() => {
55 | assertMaxWidth('100%')
56 | })
57 | })
58 |
59 | it('Renders correctly on the trailing edge', () => {
60 | // xs
61 | cy.setBreakpoint({ minWidth: 575 }).then(() => {
62 | assertMaxWidth('200px')
63 | })
64 |
65 | // sm
66 | cy.setBreakpoint({ minWidth: 576 }).then(() => {
67 | assertMaxWidth('400px')
68 | })
69 | cy.setBreakpoint({ minWidth: 767 }).then(() => {
70 | assertMaxWidth('400px')
71 | })
72 |
73 | // md
74 | cy.setBreakpoint({ minWidth: 768 }).then(() => {
75 | assertMaxWidth('400px')
76 | })
77 | cy.setBreakpoint({ minWidth: 991 }).then(() => {
78 | assertMaxWidth('400px')
79 | })
80 |
81 | // lg
82 | cy.setBreakpoint({ minWidth: 992 }).then(() => {
83 | assertMaxWidth('100%')
84 | })
85 | cy.setBreakpoint({ minWidth: 1199 }).then(() => {
86 | assertMaxWidth('100%')
87 | })
88 |
89 | // xl
90 | cy.setBreakpoint({ minWidth: 1200 }).then(() => {
91 | assertMaxWidth('100%')
92 | })
93 | })
94 | })
95 |
--------------------------------------------------------------------------------
/examples/components/Composition/rendering/responsive-props/BreakpointSpecific.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Composition } from 'atomic-layout'
3 | import Square from '@stories/Square'
4 |
5 | const BreakpointSpecific = () => (
6 |
15 | {({ First, Second }) => (
16 | <>
17 |
18 | First
19 |
20 |
21 | Second
22 |
23 | >
24 | )}
25 |
26 | )
27 |
28 | export default BreakpointSpecific
29 |
--------------------------------------------------------------------------------
/examples/components/Composition/rendering/responsive-props/BreakpointSpecific.test.js:
--------------------------------------------------------------------------------
1 | it('Breakpoint-specific', () => {
2 | cy.loadStory(
3 | ['components', 'composition', 'rendering', 'responsive-props'],
4 | ['breakpoint-specific'],
5 | )
6 |
7 | const assertGutter = (values) => {
8 | return cy.get('#composition').should('have.css', 'grid-gap', values)
9 | }
10 |
11 | assertGutter('10px 10px')
12 | cy.setBreakpoint('sm').then(() => assertGutter('20px 20px'))
13 | cy.setBreakpoint('md').then(() => assertGutter('30px 30px'))
14 | cy.setBreakpoint('lg').then(() => assertGutter('40px 40px'))
15 | cy.setBreakpoint('xl').then(() => assertGutter('50px 50px'))
16 | })
17 |
--------------------------------------------------------------------------------
/examples/components/Composition/rendering/responsive-props/InclusiveNotch.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Composition } from 'atomic-layout'
3 | import Square from '@stories/Square'
4 |
5 | const template = 'first second'
6 |
7 | const InclusiveNotch = () => (
8 | <>
9 |
16 | {({ First, Second }) => (
17 | <>
18 |
19 | First
20 |
21 |
22 | Second
23 |
24 | >
25 | )}
26 |
27 |
28 |
34 | {({ First, Second }) => (
35 | <>
36 |
37 | First
38 |
39 |
40 | Second
41 |
42 | >
43 | )}
44 |
45 | >
46 | )
47 |
48 | export default InclusiveNotch
49 |
--------------------------------------------------------------------------------
/examples/components/Composition/rendering/responsive-props/InclusiveNotch.test.js:
--------------------------------------------------------------------------------
1 | it('Bell & Notch', () => {
2 | cy.loadStory(
3 | ['components', 'composition', 'rendering', 'responsive-props'],
4 | ['inclusive-notch'],
5 | )
6 |
7 | const assertPadding = (selector, values) => {
8 | return cy.get(selector).should('have.css', 'padding', values)
9 | }
10 |
11 | assertPadding('#composition-one', '10px')
12 | cy.setBreakpoint('sm').then(() => assertPadding('#composition-one', '20px'))
13 | cy.setBreakpoint('md').then(() => assertPadding('#composition-one', '20px'))
14 | cy.setBreakpoint('lg').then(() => assertPadding('#composition-one', '20px'))
15 | cy.setBreakpoint('xl').then(() => assertPadding('#composition-one', '10px'))
16 |
17 | assertPadding('#composition-two', '10px')
18 | cy.setBreakpoint('sm').then(() => assertPadding('#composition-two', '10px'))
19 | cy.setBreakpoint('md').then(() => assertPadding('#composition-two', '20px'))
20 | cy.setBreakpoint('lg').then(() => assertPadding('#composition-two', '10px'))
21 | cy.setBreakpoint('xl').then(() => assertPadding('#composition-two', '10px'))
22 | })
23 |
--------------------------------------------------------------------------------
/examples/components/Composition/rendering/responsive-props/MobileFirst.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Composition } from 'atomic-layout'
3 | import Square from '@stories/Square'
4 |
5 | const Foo = () => (
6 |
7 | {({ First, Second }) => (
8 | <>
9 |
10 | First
11 |
12 |
13 | Second
14 |
15 | >
16 | )}
17 |
18 | )
19 |
20 | export default Foo
21 |
--------------------------------------------------------------------------------
/examples/components/Composition/rendering/responsive-props/MobileFirst.test.js:
--------------------------------------------------------------------------------
1 | it('Mobile first', () => {
2 | cy.loadStory(
3 | ['components', 'composition', 'rendering', 'responsive-props'],
4 | ['mobile-first'],
5 | )
6 |
7 | const assertGutter = () => {
8 | return cy.get('#composition').should('have.css', 'grid-gap', '10px 10px')
9 | }
10 |
11 | assertGutter()
12 | cy.setBreakpoint('sm').then(assertGutter)
13 | cy.setBreakpoint('md').then(assertGutter)
14 | cy.setBreakpoint('lg').then(assertGutter)
15 | cy.setBreakpoint('xl').then(assertGutter)
16 | })
17 |
--------------------------------------------------------------------------------
/examples/components/Only/OnlyCustomBreakpoints.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Only } from 'atomic-layout'
3 | import Square from '@stories/Square'
4 |
5 | export const breakpoints = {
6 | mobile: {
7 | minWidth: 500,
8 | maxWidth: 600,
9 | },
10 | largeMobile: {
11 | minWidth: 601,
12 | maxWidth: 700,
13 | },
14 | tablet: {
15 | minWidth: 701,
16 | maxWidth: 800,
17 | },
18 | desktop: {
19 | minWidth: 801,
20 | maxWidth: 1200,
21 | },
22 | }
23 |
24 | const OnlyCustomBreakpoint = () => (
25 |
26 | {/* Exact breakpoint */}
27 |
28 | First
29 |
30 |
31 | {/* High-pass */}
32 |
33 | Second
34 |
35 |
36 | {/* Low-pass */}
37 |
38 | Third
39 |
40 |
41 | {/* Bell */}
42 |
43 | Fourth
44 |
45 |
46 | {/* Notch */}
47 |
48 | Fifth
49 |
50 |
51 | )
52 |
53 | export default OnlyCustomBreakpoint
54 |
--------------------------------------------------------------------------------
/examples/components/Only/OnlyCustomBreakpoints.test.js:
--------------------------------------------------------------------------------
1 | describe('Supports custom breakpoints', () => {
2 | before(() => {
3 | cy.loadStory(['components', 'only'], ['custom-breakpoints'])
4 | })
5 |
6 | it('Renders children at the exact breakpoint (for)', () => {
7 | cy.setBreakpoint({ minWidth: 500 }).then(() => cy.shouldRender('#first'))
8 | cy.setBreakpoint({ minWidth: 601 }).then(() =>
9 | cy.shouldRender('#first', false),
10 | )
11 | cy.setBreakpoint({ minWidth: 701 }).then(() =>
12 | cy.shouldRender('#first', false),
13 | )
14 | cy.setBreakpoint({ minWidth: 801 }).then(() =>
15 | cy.shouldRender('#first', false),
16 | )
17 | })
18 |
19 | it('Renders children up to a given breakpoint (to)', () => {
20 | cy.setBreakpoint({ minWidth: 500 }).then(() => cy.shouldRender('#second'))
21 | cy.setBreakpoint({ minWidth: 601 }).then(() => cy.shouldRender('#second'))
22 | cy.setBreakpoint({ minWidth: 701 }).then(() =>
23 | cy.shouldRender('#second', false),
24 | )
25 | cy.setBreakpoint({ minWidth: 801 }).then(() =>
26 | cy.shouldRender('#second', false),
27 | )
28 | })
29 |
30 | it('Renders children from a given breakpoint (from)', () => {
31 | cy.setBreakpoint({ minWidth: 500 }).then(() =>
32 | cy.shouldRender('#third', false),
33 | )
34 | cy.setBreakpoint({ minWidth: 601 }).then(() =>
35 | cy.shouldRender('#third', false),
36 | )
37 | cy.setBreakpoint({ minWidth: 701 }).then(() => cy.shouldRender('#third'))
38 | cy.setBreakpoint({ minWidth: 801 }).then(() => cy.shouldRender('#third'))
39 | })
40 |
41 | it('Renders children within a breakpoint range (from/to)', () => {
42 | cy.setBreakpoint({ minWidth: 500 }).then(() =>
43 | cy.shouldRender('#fourth', false),
44 | )
45 | cy.setBreakpoint({ minWidth: 601 }).then(() => cy.shouldRender('#fourth'))
46 | cy.setBreakpoint({ minWidth: 701 }).then(() => cy.shouldRender('#fourth'))
47 | cy.setBreakpoint({ minWidth: 801 }).then(() =>
48 | cy.shouldRender('#fourth', false),
49 | )
50 | })
51 |
52 | it('Renders children excluding a breakpoint range (except)', () => {
53 | cy.setBreakpoint({ minWidth: 500 }).then(() => cy.shouldRender('#fifth'))
54 | cy.setBreakpoint({ minWidth: 601 }).then(() =>
55 | cy.shouldRender('#fifth', false),
56 | )
57 | cy.setBreakpoint({ minWidth: 701 }).then(() =>
58 | cy.shouldRender('#fifth', false),
59 | )
60 | cy.setBreakpoint({ minWidth: 801 }).then(() => cy.shouldRender('#fifth'))
61 | })
62 | })
63 |
--------------------------------------------------------------------------------
/examples/components/Only/OnlyDefaultBehavior.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Only } from 'atomic-layout'
3 | import Square from '@stories/Square'
4 |
5 | const OnlyDefault = () => (
6 | <>
7 | {/* Exact breakpoint */}
8 |
9 | For "sm"
10 |
11 |
12 | {/* High-pass */}
13 |
14 | To "md"
15 |
16 |
17 | {/* Low-pass */}
18 |
19 | From "lg"
20 |
21 |
22 | {/* Bell */}
23 |
24 | From "sm" to "lg"
25 |
26 |
27 | {/* Notch */}
28 |
29 | Except from "md" to "lg"
30 |
31 | >
32 | )
33 |
34 | export default OnlyDefault
35 |
--------------------------------------------------------------------------------
/examples/components/Only/OnlyDefaultBehavior.test.js:
--------------------------------------------------------------------------------
1 | describe('Default behavior', () => {
2 | before(() => {
3 | cy.loadStory(['components', 'only'], ['default-behavior'])
4 | })
5 |
6 | it('Renders children at the exact breakpoint (for)', () => {
7 | cy.setBreakpoint('xs').then(() => cy.shouldRender('#first', false))
8 | cy.setBreakpoint('sm').then(() => cy.shouldRender('#first'))
9 | cy.setBreakpoint('md').then(() => cy.shouldRender('#first', false))
10 | cy.setBreakpoint('lg').then(() => cy.shouldRender('#first', false))
11 | cy.setBreakpoint('xl').then(() => cy.shouldRender('#first', false))
12 | })
13 |
14 | it('Renders children up to a given breakpoint (to)', () => {
15 | cy.setBreakpoint('xs').then(() => cy.shouldRender('#second'))
16 | cy.setBreakpoint('sm').then(() => cy.shouldRender('#second'))
17 | cy.setBreakpoint('md').then(() => cy.shouldRender('#second', false))
18 | cy.setBreakpoint('lg').then(() => cy.shouldRender('#second', false))
19 | cy.setBreakpoint('xl').then(() => cy.shouldRender('#second', false))
20 | })
21 |
22 | it('Renders children from a given breakpoint (from)', () => {
23 | cy.setBreakpoint('xs').then(() => cy.shouldRender('#third', false))
24 | cy.setBreakpoint('sm').then(() => cy.shouldRender('#third', false))
25 | cy.setBreakpoint('md').then(() => cy.shouldRender('#third', false))
26 | cy.setBreakpoint('lg').then(() => cy.shouldRender('#third'))
27 | cy.setBreakpoint('xl').then(() => cy.shouldRender('#third'))
28 | })
29 |
30 | it('Renders children within a breakpoint range (from/to)', () => {
31 | cy.setBreakpoint('xs').then(() => cy.shouldRender('#fourth', false))
32 | cy.setBreakpoint('sm').then(() => cy.shouldRender('#fourth'))
33 | cy.setBreakpoint('md').then(() => cy.shouldRender('#fourth'))
34 | cy.setBreakpoint('lg').then(() => cy.shouldRender('#fourth', false))
35 | cy.setBreakpoint('xl').then(() => cy.shouldRender('#fourth', false))
36 | })
37 |
38 | it('Renders children excluding a breakpoint range (except)', () => {
39 | cy.setBreakpoint('xs').then(() => cy.shouldRender('#fifth'))
40 | cy.setBreakpoint('sm').then(() => cy.shouldRender('#fifth'))
41 | cy.setBreakpoint('md').then(() => cy.shouldRender('#fifth', false))
42 | cy.setBreakpoint('lg').then(() => cy.shouldRender('#fifth'))
43 | cy.setBreakpoint('xl').then(() => cy.shouldRender('#fifth'))
44 | })
45 | })
46 |
--------------------------------------------------------------------------------
/examples/components/Visible/VisibleDefaultBehavior.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Visible } from 'atomic-layout'
3 | import Square from '@stories/Square'
4 |
5 | const VisibleDefaultBehavior = () => (
6 | <>
7 | {/* Explicit breakpoint */}
8 |
9 | For "sm"
10 |
11 | {/* High-pass */}
12 |
13 | To "md"
14 |
15 | {/* Low-pass */}
16 |
17 | From "lg"
18 |
19 | {/* Bell */}
20 |
21 | From "sm" to "lg"
22 |
23 | {/* Notch */}
24 |
25 | Except from "md" to "lg"
26 |
27 | >
28 | )
29 |
30 | export default VisibleDefaultBehavior
31 |
--------------------------------------------------------------------------------
/examples/configuration/CustomBreakpoints.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Layout, { Composition } from 'atomic-layout'
3 | import Square from '@stories/Square'
4 |
5 | const areas = `
6 | first
7 | second
8 | `
9 |
10 | export default class CustomBreakpoints extends React.Component {
11 | componentWillMount() {
12 | Layout.configure({
13 | defaultUnit: 'rem',
14 | defaultBreakpointName: 'mobile',
15 | breakpoints: {
16 | mobile: {
17 | maxWidth: '575px',
18 | },
19 | tablet: {
20 | minWidth: '576px',
21 | maxWidth: '768px',
22 | },
23 | desktop: {
24 | minWidth: '769px',
25 | },
26 | },
27 | })
28 | }
29 |
30 | render() {
31 | return (
32 |
40 | {({ First, Second }) => (
41 | <>
42 |
43 | First
44 |
45 |
46 | Second
47 |
48 | >
49 | )}
50 |
51 | )
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/examples/configuration/CustomBreakpoints.test.js:
--------------------------------------------------------------------------------
1 | it('Supports custom breakpoints', () => {
2 | cy.loadStory(
3 | ['configuration', 'custom-configuration'],
4 | ['custom-breakpoints'],
5 | )
6 |
7 | const assertPadding = (values) => {
8 | return cy.get('#composition').should('have.css', 'padding', values)
9 | }
10 |
11 | assertPadding('16px')
12 |
13 | cy.setBreakpoint({
14 | minWidth: 576,
15 | maxWidth: 768,
16 | }).then(() => assertPadding('32px'))
17 |
18 | cy.setBreakpoint({
19 | minWidth: 769,
20 | }).then(() => assertPadding('48px'))
21 | })
22 |
--------------------------------------------------------------------------------
/examples/configuration/CustomUnit.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Layout from 'atomic-layout'
3 | import Notch from '../components/Composition/rendering/behaviors/Notch'
4 |
5 | export default class CustomUnit extends React.Component {
6 | componentWillMount() {
7 | Layout.configure({
8 | defaultUnit: 'rem',
9 | })
10 | }
11 |
12 | render() {
13 | return
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/examples/configuration/CustomUnit.test.js:
--------------------------------------------------------------------------------
1 | it('Supports custom measurement unit', () => {
2 | cy.loadStory(['configuration', 'custom-configuration'], ['custom-unit'])
3 |
4 | cy.get('#notch').assertNotch()
5 | })
6 |
--------------------------------------------------------------------------------
/examples/hooks/UseBreakpointChange.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import { useBreakpointChange } from 'atomic-layout'
3 |
4 | const UseBreakpointChange = () => {
5 | const [text, setText] = useState('default')
6 |
7 | useBreakpointChange((breakpointName) => {
8 | setText(breakpointName)
9 | })
10 |
11 | return (
12 |
15 | )
16 | }
17 |
18 | export default UseBreakpointChange
19 |
--------------------------------------------------------------------------------
/examples/hooks/UseResponsiveProps.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'supported-styling-library'
3 | import { useResponsiveProps } from 'atomic-layout'
4 |
5 | const Avatar = styled.div`
6 | position: relative;
7 | background-color: #ccc;
8 | border-radius: 50%;
9 | height: ${({ size }) => size}px;
10 | width: ${({ size }) => size}px;
11 | `
12 |
13 | const Badge = styled.span`
14 | display: inline-block;
15 | background-color: green;
16 | border-radius: 50%;
17 | border: 3px solid #fff;
18 | height: 10px;
19 | width: 10px;
20 | `
21 |
22 | const Element = (props) => {
23 | const { size, showBadge, fontSize } = useResponsiveProps(props)
24 |
25 | return (
26 |
27 |
28 | {showBadge && }
29 |
30 |
31 | )
32 | }
33 |
34 | const UseResponsivePropsExample = () => (
35 |
48 | )
49 |
50 | export default UseResponsivePropsExample
51 |
--------------------------------------------------------------------------------
/examples/hooks/UseResponsiveValue.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useResponsiveValue, Box } from 'atomic-layout'
3 |
4 | const UseResponsiveValue = () => {
5 | const text = useResponsiveValue(
6 | {
7 | xs: 'xs',
8 | md: 'md',
9 | lg: 'lg',
10 | },
11 | 'default',
12 | )
13 |
14 | return {text}
15 | }
16 |
17 | export default UseResponsiveValue
18 |
--------------------------------------------------------------------------------
/examples/hooks/UseViewportChange.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import Layout, { useViewportChange, Box } from 'atomic-layout'
3 |
4 | const UseViewportChange = () => {
5 | const [isVisible, setVisibility] = useState(false)
6 |
7 | useViewportChange(() => {
8 | setVisibility(
9 | matchMedia(`(min-width: ${Layout.breakpoints.lg.minWidth})`).matches,
10 | )
11 | })
12 |
13 | return (
14 | isVisible && (
15 |
16 | Content
17 |
18 | )
19 | )
20 | }
21 |
22 | export default UseViewportChange
23 |
--------------------------------------------------------------------------------
/examples/hooks/useBreakpointChange.test.js:
--------------------------------------------------------------------------------
1 | describe('useBreakpointChange', () => {
2 | before(() => {
3 | cy.loadStory(['hooks'], ['usebreakpointchange'])
4 | })
5 |
6 | const assertText = (value) => {
7 | return cy.get('#element').should('have.text', value)
8 | }
9 |
10 | it('Renders with the correct initial state', () => {
11 | assertText('default')
12 | })
13 |
14 | it('Sets breakpoint name on breakpoint change', () => {
15 | cy.setBreakpoint('sm').then(() => {
16 | assertText('sm')
17 | })
18 | })
19 |
20 | it('Ignores resize within the same breakpoint', () => {
21 | cy.viewport(580, 700).then(() => {
22 | assertText('sm')
23 | })
24 |
25 | cy.viewport(600, 700).then(() => {
26 | assertText('sm')
27 | })
28 | })
29 |
30 | it('Re-renders on breakpoint change', () => {
31 | cy.setBreakpoint('md').then(() => {
32 | assertText('md')
33 | })
34 | })
35 | })
36 |
--------------------------------------------------------------------------------
/examples/hooks/useResponsiveProps.test.js:
--------------------------------------------------------------------------------
1 | describe('useResponsiveProps', () => {
2 | before(() => {
3 | cy.loadStory(['hooks'], ['useresponsiveprops'])
4 | })
5 |
6 | beforeEach(() => {
7 | cy.setBreakpoint('xs')
8 | })
9 |
10 | const hasSize = (size) => {
11 | return cy
12 | .get('#avatar')
13 | .should('have.css', 'height', `${size}px`)
14 | .should('have.css', 'width', `${size}px`)
15 | }
16 |
17 | const hasFontSize = (size) => {
18 | return cy.get('#element').should('have.css', 'font-size', `${size}px`)
19 | }
20 |
21 | const hasBadge = (visible = true) => {
22 | return cy.get('#badge').should(visible ? 'be.visible' : 'not.be.visible')
23 | }
24 |
25 | it('High-pass', () => {
26 | hasSize(50)
27 | cy.setBreakpoint('sm').then(() => hasSize(50))
28 | cy.setBreakpoint('md').then(() => hasSize(75))
29 | cy.setBreakpoint('lg').then(() => hasSize(100))
30 | cy.setBreakpoint('xl').then(() => hasSize(100))
31 | })
32 |
33 | it('Bell', () => {
34 | hasFontSize(16)
35 | cy.setBreakpoint('sm').then(() => hasFontSize(20))
36 | cy.setBreakpoint('md').then(() => hasFontSize(20))
37 | cy.setBreakpoint('lg').then(() => hasFontSize(16))
38 | cy.setBreakpoint('xl').then(() => hasFontSize(16))
39 | })
40 |
41 | it('Notch', () => {
42 | hasBadge()
43 | cy.setBreakpoint('sm').then(() => hasBadge())
44 | cy.setBreakpoint('md').then(() => hasBadge())
45 | cy.setBreakpoint('lg').then(() => hasBadge(false))
46 | cy.setBreakpoint('xl').then(() => hasBadge(false))
47 | })
48 | })
49 |
--------------------------------------------------------------------------------
/examples/hooks/useResponsiveValue.test.js:
--------------------------------------------------------------------------------
1 | describe('useResponsiveValue', () => {
2 | before(() => {
3 | cy.loadStory(['hooks'], ['useresponsivevalue'])
4 | })
5 |
6 | const assertText = (value) => {
7 | return cy.get('#element').should('have.text', value)
8 | }
9 |
10 | it('Resolves to a value associated with a breakpoint', () => {
11 | assertText('xs')
12 | cy.setBreakpoint('md').then(() => assertText('md'))
13 | cy.setBreakpoint('lg').then(() => assertText('lg'))
14 | })
15 |
16 | it('Fallbacks to the default value when no breakpoint association found', () => {
17 | cy.setBreakpoint('sm').then(() => assertText('default'))
18 | cy.setBreakpoint('xl').then(() => assertText('default'))
19 | })
20 | })
21 |
--------------------------------------------------------------------------------
/examples/hooks/useViewportChange.test.js:
--------------------------------------------------------------------------------
1 | describe('useViewportChange', () => {
2 | before(() => {
3 | cy.loadStory(['hooks'], ['useviewportchange'])
4 | })
5 |
6 | const element = () => cy.get('#element')
7 |
8 | it('Renders with the correct initial state', () => {
9 | element().should('not.be.visible')
10 | })
11 |
12 | it('Does not render at unmatching viewport', () => {
13 | cy.setBreakpoint('sm').then(() => element().should('not.be.visible'))
14 | cy.setBreakpoint('md').then(() => element().should('not.be.visible'))
15 | })
16 |
17 | it('Renders when the viewport matches', () => {
18 | cy.setBreakpoint('lg').then(() => element().should('be.visible'))
19 | cy.setBreakpoint('xl').then(() => element().should('be.visible'))
20 | })
21 |
22 | it('Does not render when returned to non-matching viewport', () => {
23 | cy.setBreakpoint('sm').then(() => element().should('not.be.visible'))
24 | cy.setBreakpoint('md').then(() => element().should('not.be.visible'))
25 | })
26 | })
27 |
--------------------------------------------------------------------------------
/examples/recipes/IterativeAreas.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Composition } from 'atomic-layout'
3 | import Square from '@stories/Square'
4 |
5 | const dataOne = [
6 | { id: 'abc', text: 'Foo' },
7 | { id: 'def', text: 'Bar' },
8 | { id: 'ghi', text: 'Doe' },
9 | ]
10 |
11 | const dataTwo = [
12 | { id: 123, text: '123' },
13 | { id: 456, text: '456' },
14 | { id: 789, text: '789' },
15 | ]
16 |
17 | const List = () => (
18 | <>
19 | Columns:
20 |
21 | {({ SingleArea }) =>
22 | dataOne.map((entry) => (
23 |
24 | {entry.text}
25 |
26 | ))
27 | }
28 |
29 |
30 | Rows:
31 |
32 | {({ SingleArea }) =>
33 | dataTwo.map((entry) => (
34 |
35 | {entry.text}
36 |
37 | ))
38 | }
39 |
40 | >
41 | )
42 |
43 | export default List
44 |
--------------------------------------------------------------------------------
/examples/recipes/IterativeAreas.test.js:
--------------------------------------------------------------------------------
1 | describe('Iterative areas', function() {
2 | before(() => {
3 | cy.loadStory(['recipes', 'all'], ['iterative-areas'])
4 | })
5 |
6 | describe('Iterative columns', function() {
7 | it('Renders without crashing', () => {
8 | cy.get('#composition-one').should('be.visible')
9 | cy.get('#abc').should('be.visible')
10 | cy.get('#def').should('be.visible')
11 | cy.get('#ghi').should('be.visible')
12 | })
13 |
14 | it('Respects col="auto" on dynamic area', () => {
15 | cy.get('#abc')
16 | .should('have.css', 'grid-column', 'auto / auto')
17 | .should('have.css', 'grid-row', 'singleArea / singleArea')
18 | .notIntersectWith('#def')
19 | .notIntersectWith('#ghi')
20 |
21 | cy.get('#def')
22 | .should('have.css', 'grid-column', 'auto / auto')
23 | .should('have.css', 'grid-row', 'singleArea / singleArea')
24 | .notIntersectWith('#abc')
25 | .notIntersectWith('#ghi')
26 |
27 | cy.get('#ghi')
28 | .should('have.css', 'grid-column', 'auto / auto')
29 | .should('have.css', 'grid-row', 'singleArea / singleArea')
30 | .notIntersectWith('#abc')
31 | .notIntersectWith('#def')
32 | })
33 | })
34 |
35 | describe('Iterative rows', function() {
36 | it('Renders without crashing', () => {
37 | cy.get('#composition-two').should('be.visible')
38 | cy.get('#123').should('be.visible')
39 | cy.get('#456').should('be.visible')
40 | cy.get('#789').should('be.visible')
41 | })
42 |
43 | it('Respects row="auto" on dynamic area', () => {
44 | cy.get('#123')
45 | .should('have.css', 'grid-column', 'singleArea / singleArea')
46 | .should('have.css', 'grid-row', 'auto / auto')
47 | .notIntersectWith('#456')
48 | .notIntersectWith('#789')
49 |
50 | cy.get('#456')
51 | .should('have.css', 'grid-column', 'singleArea / singleArea')
52 | .should('have.css', 'grid-row', 'auto / auto')
53 | .notIntersectWith('#123')
54 | .notIntersectWith('#789')
55 |
56 | cy.get('#789')
57 | .should('have.css', 'grid-column', 'singleArea / singleArea')
58 | .should('have.css', 'grid-row', 'auto / auto')
59 | .notIntersectWith('#123')
60 | .notIntersectWith('#456')
61 | })
62 | })
63 | })
64 |
--------------------------------------------------------------------------------
/examples/regression/DisplayNames.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Box, Composition, Only, Visible } from 'atomic-layout'
3 |
4 | const DisplayNames = () => (
5 |
6 |
{Box.displayName}
7 |
12 | {(Areas) => (
13 | {Areas.One.displayName}
14 | )}
15 |
16 |
17 | {Only.displayName}
18 |
19 |
20 | {Visible.displayName}
21 |
22 |
23 | )
24 |
25 | export default DisplayNames
26 |
--------------------------------------------------------------------------------
/examples/regression/DisplayNames.test.js:
--------------------------------------------------------------------------------
1 | describe('Display names', () => {
2 | before(() => {
3 | cy.loadStory(['regression', 'all'], ['display-names'])
4 | })
5 |
6 | it('Box should have a meaningful display name', () => {
7 | cy.get('#box').should('have.text', 'Box')
8 | })
9 |
10 | it('Composition should have meaningful display name', () => {
11 | cy.get('#composition').should(
12 | 'have.attr',
13 | 'data-display-name',
14 | 'Composition',
15 | )
16 | })
17 |
18 | it('Composition areas should have meaningful display name', () => {
19 | cy.get('#composition-area').should('have.text', 'Area(One)')
20 | })
21 |
22 | it('Only should have meaningful display name', () => {
23 | cy.get('#only').should('have.text', 'Only')
24 | })
25 |
26 | it('Visible should have meaningful display name', () => {
27 | cy.get('#visible').should('have.text', 'Visible')
28 | })
29 | })
30 |
--------------------------------------------------------------------------------
/examples/regression/OnlyUnmount.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import { Only } from 'atomic-layout'
3 |
4 | const Child = ({ disabled }) => {
5 | const [count, setCount] = useState(0)
6 |
7 | return (
8 |
9 |
10 | Count: {count}
11 |
12 |
setCount(count + 1)}
16 | >
17 | Increase counter
18 |
19 |
20 | )
21 | }
22 |
23 | const OnlyUnmount = () => {
24 | const [isDisabled, setDisabled] = useState(false)
25 |
26 | return (
27 |
28 |
29 |
30 |
31 | setDisabled(!isDisabled)}>
32 | Toggle disabled
33 |
34 |
35 | )
36 | }
37 |
38 | export default OnlyUnmount
39 |
--------------------------------------------------------------------------------
/examples/regression/OnlyUnmount.test.js:
--------------------------------------------------------------------------------
1 | describe('Only unmount', () => {
2 | before(() => {
3 | cy.loadStory(['regression', 'all'], ['only-unmount'])
4 | })
5 |
6 | describe('given I clicked on increment counter', () => {
7 | before(() => {
8 | cy.get('#button-increment')
9 | .click()
10 | .click()
11 | .click()
12 | })
13 |
14 | it('should update the counter state', () => {
15 | cy.get('#count').should('have.text', '3')
16 | })
17 |
18 | it('should retain state when parent updates', () => {
19 | cy.get('#button-disable').click()
20 | cy.get('#count').should('have.text', '3')
21 | })
22 | })
23 | })
24 |
--------------------------------------------------------------------------------
/examples/regression/ParentRerendering.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import { Composition } from 'atomic-layout'
3 |
4 | const ParentRerendering = () => {
5 | const [value, setValue] = useState('')
6 |
7 | return (
8 |
9 | {(Areas) => (
10 | <>
11 |
12 | Title
13 |
14 |
15 | setValue(event.target.value)}
19 | />
20 |
21 | >
22 | )}
23 |
24 | )
25 | }
26 |
27 | export default ParentRerendering
28 |
--------------------------------------------------------------------------------
/examples/regression/ParentRerendering.test.js:
--------------------------------------------------------------------------------
1 | describe('Parent re-rendering', () => {
2 | before(() => {
3 | cy.loadStory(['regression', 'all'], ['parent-re-rendering'])
4 | })
5 |
6 | it('Should preserve areas state when parent updates', () => {
7 | cy.get('input[name="username"]')
8 | .type('admin')
9 | .should('be.focused')
10 | .should('have.value', 'admin')
11 | })
12 | })
13 |
--------------------------------------------------------------------------------
/examples/regression/SingleResponsiveProp.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Composition } from 'atomic-layout'
3 | import Square from '@stories/Square'
4 |
5 | const SingleResponsiveProp = () => (
6 |
12 | {(Areas) => (
13 | <>
14 |
15 | Left
16 |
17 |
18 | Right
19 |
20 | >
21 | )}
22 |
23 | )
24 |
25 | export default SingleResponsiveProp
26 |
--------------------------------------------------------------------------------
/examples/regression/SingleResponsiveProp.test.js:
--------------------------------------------------------------------------------
1 | describe('Single responsive prop', () => {
2 | before(() => {
3 | cy.loadStory(['regression', 'all'], ['single-responsive-prop'])
4 | })
5 |
6 | describe('Given "up" behavior', () => {
7 | it('Renders no areas on unmatched breakpoints', () => {
8 | const shouldRenderNothing = () => {
9 | cy.get('[data-area="left"]').should('not.be.visible')
10 | cy.get('[data-area="right"]').should('not.be.visible')
11 | }
12 |
13 | cy.setBreakpoint('xs').then(shouldRenderNothing)
14 | cy.setBreakpoint('sm').then(shouldRenderNothing)
15 | })
16 |
17 | it('Renders areas on matched breakpoint and up', () => {
18 | const shouldRenderAreas = () => {
19 | cy.get('#composition').assertAreas([['left', 'right']])
20 | }
21 |
22 | cy.setBreakpoint('md').then(shouldRenderAreas)
23 | cy.setBreakpoint('lg').then(shouldRenderAreas)
24 | cy.setBreakpoint('xl').then(shouldRenderAreas)
25 | })
26 | })
27 |
28 | describe('Given "down" behavior', () => {
29 | it('Has no padding on unmatched breakpoints', () => {
30 | const shouldHaveNoPadding = () => {
31 | cy.get('#composition').should('have.css', 'padding', '0px')
32 | }
33 |
34 | cy.setBreakpoint('lg').then(shouldHaveNoPadding)
35 | cy.setBreakpoint('xl').then(shouldHaveNoPadding)
36 | })
37 |
38 | it('Has padding on matched breakpoints', () => {
39 | const shouldHavePadding = () => {
40 | cy.get('#composition').should('have.css', 'padding', '15px')
41 | }
42 |
43 | cy.setBreakpoint('xs').then(shouldHavePadding)
44 | cy.setBreakpoint('sm').then(shouldHavePadding)
45 | cy.setBreakpoint('md').then(shouldHavePadding)
46 | })
47 | })
48 |
49 | describe('Given "only" behavior', () => {
50 | it('Has no gap on unmatched breakpoints', () => {
51 | const shouldHaveNoGap = () => {
52 | cy.get('#composition').should('have.css', 'grid-gap', 'normal normal')
53 | }
54 |
55 | cy.setBreakpoint('xs').then(shouldHaveNoGap)
56 | cy.setBreakpoint('sm').then(shouldHaveNoGap)
57 | cy.setBreakpoint('md').then(shouldHaveNoGap)
58 | })
59 |
60 | it('Has a gap on a matched breakpoint', () => {
61 | const shouldHaveGap = () => {
62 | cy.get('#composition').should('have.css', 'grid-gap', '10px 10px')
63 | }
64 |
65 | cy.setBreakpoint('lg').then(shouldHaveGap)
66 | })
67 | })
68 | })
69 |
--------------------------------------------------------------------------------
/examples/regression/StylesUndefined.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Composition } from 'atomic-layout'
3 |
4 | const TemplateIndentation = () => (
5 |
6 |
7 | Must not contain gutter
CSS property.
8 |
9 |
10 | )
11 |
12 | export default TemplateIndentation
13 |
--------------------------------------------------------------------------------
/examples/regression/StylesUndefined.test.js:
--------------------------------------------------------------------------------
1 | it('Omits style props with "undefined" value', () => {
2 | cy.loadStory(['regression', 'all'], ['styles-undefined'])
3 |
4 | cy.get('#composition')
5 | .should('be.visible')
6 | .should('have.css', 'padding-right', '12px')
7 | .should('not.have.css', 'gutter')
8 | })
9 |
--------------------------------------------------------------------------------
/examples/semantics/PolymorphicProp.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'supported-styling-library'
3 | import { Composition } from 'atomic-layout'
4 | import Square from '@stories/Square'
5 |
6 | const CustomHeader = styled.header`
7 | background-color: #eee;
8 | display: block;
9 | padding: 10px;
10 | `
11 |
12 | const PolymorphicProp = () => (
13 |
14 | {({ Header, Main, Element, Footer }) => (
15 | <>
16 |
19 |
20 | Main
21 |
22 |
23 | Default element
24 |
25 |
28 | >
29 | )}
30 |
31 | )
32 |
33 | export default PolymorphicProp
34 |
--------------------------------------------------------------------------------
/examples/semantics/PolymorphicProp.test.js:
--------------------------------------------------------------------------------
1 | describe('Polymorphic prop', () => {
2 | before(() => {
3 | cy.loadStory(['semantics'], ['polymorphic-prop'])
4 | })
5 |
6 | it('Renders "div" by default', () => {
7 | cy.get('#default').haveTag('div')
8 | })
9 |
10 | describe('When given a tag name string', () => {
11 | it('Renders the given tag', () => {
12 | cy.get('#main').haveTag('main')
13 | cy.get('#footer').haveTag('footer')
14 | })
15 | })
16 |
17 | describe('When given a custom React component', () => {
18 | it('Renders a custom React component', () => {
19 | cy.get('#header').haveTag('header')
20 | })
21 |
22 | it('Preserves irrelevant styles', () => {
23 | cy.get('#header').should(
24 | 'have.css',
25 | 'background-color',
26 | 'rgb(238, 238, 238)',
27 | )
28 | })
29 |
30 | it('Overrides layout styles', () => {
31 | cy.get('#header')
32 | .should('have.css', 'display', 'block')
33 | .should('have.css', 'padding', '20px')
34 | })
35 | })
36 | })
37 |
--------------------------------------------------------------------------------
/examples/styles.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
3 | Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
4 | font-size: 14px;
5 | }
6 |
--------------------------------------------------------------------------------
/examples/utils/MakeResponsive/MakeResponsive.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'supported-styling-library'
3 | import { makeResponsive } from 'atomic-layout'
4 |
5 | const Text = makeResponsive(
6 | styled.h1`
7 | color: ${({ color }) => color};
8 | font-size: ${({ size }) => size}px;
9 | `,
10 | )
11 |
12 | const Scenario = () => {
13 | const textRef = React.useRef({})
14 | const [state, setState] = React.useState('red')
15 | const [refDerivedData, setRefDerivedData] = React.useState(null)
16 |
17 | React.useEffect(() => {
18 | setRefDerivedData(textRef.current.id)
19 | }, [textRef.current])
20 |
21 | return (
22 |
23 |
33 |
34 | Quick brown fox (#{refDerivedData} )
35 |
36 | setState('violet')}>
37 | Make violet
38 |
39 |
40 |
41 | )
42 | }
43 |
44 | export default Scenario
45 |
--------------------------------------------------------------------------------
/examples/utils/MakeResponsive/makeResponsive.test.js:
--------------------------------------------------------------------------------
1 | describe('makeResponsive', () => {
2 | before(() => {
3 | cy.loadStory(['utilities'], ['makeresponsive'])
4 | })
5 |
6 | describe('Prmiary behavior', () => {
7 | it('Custom responsive prop has correct initial state', () => {
8 | cy.get('#text')
9 | .should('have.css', 'font-size', '16px')
10 | .should('have.css', 'color', 'rgb(0, 0, 0)')
11 | })
12 |
13 | it('Custom responsive prop changes correctly on breakpoint changes', () => {
14 | cy.setBreakpoint('sm').then(() => {
15 | cy.get('#text')
16 | .should('have.css', 'font-size', '16px')
17 | .should('have.css', 'color', 'rgb(0, 128, 0)')
18 | })
19 |
20 | cy.setBreakpoint('md').then(() => {
21 | cy.get('#text')
22 | .should('have.css', 'font-size', '20px')
23 | .should('have.css', 'color', 'rgb(0, 128, 0)')
24 | })
25 |
26 | cy.setBreakpoint('lg').then(() => {
27 | cy.get('#text')
28 | .should('have.css', 'font-size', '24px')
29 | .should('have.css', 'color', 'rgb(255, 0, 0)')
30 | })
31 |
32 | cy.setBreakpoint('xl').then(() => {
33 | cy.get('#text')
34 | .should('have.css', 'font-size', '24px')
35 | .should('have.css', 'color', 'rgb(255, 0, 0)')
36 | })
37 | })
38 | })
39 |
40 | describe('Generic behavior', () => {
41 | it('Supports ref forwarding', () => {
42 | cy.get('#ref').should('have.text', '#text')
43 | })
44 |
45 | it('Re-renders on props change', () => {
46 | cy.setBreakpoint('lg')
47 | cy.get('#change-color').click()
48 | cy.get('#text').should('have.css', 'color', 'rgb(238, 130, 238)')
49 |
50 | // Test that the updated value persists after breakpoint change.
51 | // This ensures that "useResponsiveProps" handles both direct
52 | // and responsive props updates.
53 | cy.setBreakpoint('md')
54 | cy.setBreakpoint('lg')
55 | cy.get('#text').should('have.css', 'color', 'rgb(238, 130, 238)')
56 | })
57 | })
58 | })
59 |
--------------------------------------------------------------------------------
/examples/utils/Query/Query.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'supported-styling-library'
3 | import { query } from 'atomic-layout'
4 |
5 | const Component = styled.div`
6 | font-size: 14px;
7 |
8 | /* Single breakpoint */
9 | @media ${query({ for: 'sm' })} {
10 | color: cyan;
11 | }
12 |
13 | /* High-pass */
14 | @media ${query({ from: 'md' })} {
15 | background-color: lightcyan;
16 | }
17 |
18 | /* Low-pass */
19 | @media ${query({ to: 'sm' })} {
20 | padding: 10px;
21 | }
22 |
23 | /* Bell */
24 | @media ${query({ from: 'md', to: 'xl' })} {
25 | margin: 20px;
26 | }
27 |
28 | /* Notch */
29 | @media ${query({ except: true, from: 'md', to: 'lg' })} {
30 | font-size: 18px;
31 | }
32 | `
33 |
34 | const QueryExample = () => {
35 | return Content
36 | }
37 |
38 | export default QueryExample
39 |
--------------------------------------------------------------------------------
/examples/utils/Query/query.test.js:
--------------------------------------------------------------------------------
1 | describe('query()', () => {
2 | before(() => {
3 | cy.loadStory(['utilities'], ['query'])
4 | })
5 |
6 | const assertCss = (propertyName, value) => {
7 | return cy
8 | .get('[data-test-id="component"]')
9 | .should('have.css', propertyName, value)
10 | }
11 |
12 | it('Supports an exact breakpoint (for)', () => {
13 | cy.setBreakpoint('xs').then(() => assertCss('color', 'rgb(0, 0, 0)'))
14 | cy.setBreakpoint('sm').then(() => assertCss('color', 'rgb(0, 255, 255)'))
15 | cy.setBreakpoint('md').then(() => assertCss('color', 'rgb(0, 0, 0)'))
16 | cy.setBreakpoint('lg').then(() => assertCss('color', 'rgb(0, 0, 0)'))
17 | cy.setBreakpoint('xl').then(() => assertCss('color', 'rgb(0, 0, 0)'))
18 | })
19 |
20 | it('Supports a high-pass breakpoint range (from)', () => {
21 | cy.setBreakpoint('xs').then(() =>
22 | assertCss('background-color', 'rgba(0, 0, 0, 0)'),
23 | )
24 | cy.setBreakpoint('sm').then(() =>
25 | assertCss('background-color', 'rgba(0, 0, 0, 0)'),
26 | )
27 | cy.setBreakpoint('md').then(() =>
28 | assertCss('background-color', 'rgb(224, 255, 255)'),
29 | )
30 | cy.setBreakpoint('lg').then(() =>
31 | assertCss('background-color', 'rgb(224, 255, 255)'),
32 | )
33 | cy.setBreakpoint('xl').then(() =>
34 | assertCss('background-color', 'rgb(224, 255, 255)'),
35 | )
36 | })
37 |
38 | it('Supports a low-pass breakpoint range (to)', () => {
39 | cy.setBreakpoint('xs').then(() => assertCss('padding', '10px'))
40 | cy.setBreakpoint('sm').then(() => assertCss('padding', '0px'))
41 | cy.setBreakpoint('md').then(() => assertCss('padding', '0px'))
42 | cy.setBreakpoint('lg').then(() => assertCss('padding', '0px'))
43 | cy.setBreakpoint('xl').then(() => assertCss('padding', '0px'))
44 | })
45 |
46 | it('Supports a bell breakpoint range (from/to)', () => {
47 | cy.setBreakpoint('xs').then(() => assertCss('margin', '0px'))
48 | cy.setBreakpoint('sm').then(() => assertCss('margin', '0px'))
49 | cy.setBreakpoint('md').then(() => assertCss('margin', '20px'))
50 | cy.setBreakpoint('lg').then(() => assertCss('margin', '20px'))
51 | cy.setBreakpoint('xl').then(() => assertCss('margin', '0px'))
52 | })
53 |
54 | it('Supports a notch breakpoint range (except/from/to)', () => {
55 | cy.setBreakpoint('xs').then(() => assertCss('font-size', '18px'))
56 | cy.setBreakpoint('sm').then(() => assertCss('font-size', '18px'))
57 | cy.setBreakpoint('md').then(() => assertCss('font-size', '14px'))
58 | cy.setBreakpoint('lg').then(() => assertCss('font-size', '18px'))
59 | cy.setBreakpoint('xl').then(() => assertCss('font-size', '18px'))
60 | })
61 | })
62 |
--------------------------------------------------------------------------------
/lerna.json:
--------------------------------------------------------------------------------
1 | {
2 | "packages": ["packages/*"],
3 | "version": "independent",
4 | "npmClient": "yarn",
5 | "useWorkspaces": true
6 | }
7 |
--------------------------------------------------------------------------------
/logo-full.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kettanaito/atomic-layout/3df0c1c33739f627db72d03a005b4ee9c7b8aad6/logo-full.png
--------------------------------------------------------------------------------
/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/materials/example-thumbnails/basic-composition.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kettanaito/atomic-layout/3df0c1c33739f627db72d03a005b4ee9c7b8aad6/materials/example-thumbnails/basic-composition.jpg
--------------------------------------------------------------------------------
/materials/example-thumbnails/codesandbox/thumbnail-blue.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kettanaito/atomic-layout/3df0c1c33739f627db72d03a005b4ee9c7b8aad6/materials/example-thumbnails/codesandbox/thumbnail-blue.jpg
--------------------------------------------------------------------------------
/materials/example-thumbnails/codesandbox/thumbnail-violet.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kettanaito/atomic-layout/3df0c1c33739f627db72d03a005b4ee9c7b8aad6/materials/example-thumbnails/codesandbox/thumbnail-violet.jpg
--------------------------------------------------------------------------------
/materials/example-thumbnails/codesandbox/thumbnail.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kettanaito/atomic-layout/3df0c1c33739f627db72d03a005b4ee9c7b8aad6/materials/example-thumbnails/codesandbox/thumbnail.jpg
--------------------------------------------------------------------------------
/materials/example-thumbnails/conditional-rendering.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kettanaito/atomic-layout/3df0c1c33739f627db72d03a005b4ee9c7b8aad6/materials/example-thumbnails/conditional-rendering.jpg
--------------------------------------------------------------------------------
/materials/example-thumbnails/custom-configuration.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kettanaito/atomic-layout/3df0c1c33739f627db72d03a005b4ee9c7b8aad6/materials/example-thumbnails/custom-configuration.jpg
--------------------------------------------------------------------------------
/materials/example-thumbnails/nested-composition.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kettanaito/atomic-layout/3df0c1c33739f627db72d03a005b4ee9c7b8aad6/materials/example-thumbnails/nested-composition.jpg
--------------------------------------------------------------------------------
/materials/example-thumbnails/responsive-props.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kettanaito/atomic-layout/3df0c1c33739f627db72d03a005b4ee9c7b8aad6/materials/example-thumbnails/responsive-props.jpg
--------------------------------------------------------------------------------
/materials/example-thumbnails/shorthand-media-query.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kettanaito/atomic-layout/3df0c1c33739f627db72d03a005b4ee9c7b8aad6/materials/example-thumbnails/shorthand-media-query.jpg
--------------------------------------------------------------------------------
/materials/github/contrib-e2e-test.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kettanaito/atomic-layout/3df0c1c33739f627db72d03a005b4ee9c7b8aad6/materials/github/contrib-e2e-test.png
--------------------------------------------------------------------------------
/materials/github/contrib-new-story.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kettanaito/atomic-layout/3df0c1c33739f627db72d03a005b4ee9c7b8aad6/materials/github/contrib-new-story.png
--------------------------------------------------------------------------------
/materials/github/contrib-unit-test.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kettanaito/atomic-layout/3df0c1c33739f627db72d03a005b4ee9c7b8aad6/materials/github/contrib-unit-test.png
--------------------------------------------------------------------------------
/materials/github/gitub-preview.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kettanaito/atomic-layout/3df0c1c33739f627db72d03a005b4ee9c7b8aad6/materials/github/gitub-preview.jpg
--------------------------------------------------------------------------------
/materials/react-finland-thumbnail.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kettanaito/atomic-layout/3df0c1c33739f627db72d03a005b4ee9c7b8aad6/materials/react-finland-thumbnail.jpg
--------------------------------------------------------------------------------
/materials/react-vienna-thumbnail.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kettanaito/atomic-layout/3df0c1c33739f627db72d03a005b4ee9c7b8aad6/materials/react-vienna-thumbnail.jpg
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "atomic-layout",
3 | "description": "Physical representation of layout composition to create declarative, responsive layouts in React.",
4 | "license": "MIT",
5 | "private": true,
6 | "workspaces": {
7 | "packages": [
8 | "packages/*"
9 | ]
10 | },
11 | "scripts": {
12 | "lint": "lerna exec yarn lint",
13 | "clean": "lerna exec yarn clean",
14 | "build": "lerna exec yarn build",
15 | "test": "lerna exec yarn test",
16 | "verify": "lerna exec yarn verify"
17 | },
18 | "husky": {
19 | "hooks": {
20 | "pre-commit": "lint-staged"
21 | }
22 | },
23 | "lint-staged": {
24 | "*.{ts,tsx}": [
25 | "yarn lint",
26 | "prettier --write",
27 | "git add"
28 | ]
29 | },
30 | "devDependencies": {
31 | "@babel/core": "7.7.2",
32 | "@babel/plugin-proposal-class-properties": "7.7.0",
33 | "@babel/plugin-proposal-export-default-from": "7.5.2",
34 | "@babel/plugin-proposal-export-namespace-from": "7.5.2",
35 | "@babel/plugin-proposal-object-rest-spread": "7.6.2",
36 | "@babel/preset-env": "7.7.4",
37 | "@babel/preset-react": "7.7.4",
38 | "@babel/preset-typescript": "7.7.4",
39 | "@storybook/addon-actions": "5.3.7",
40 | "@storybook/addon-links": "5.3.7",
41 | "@storybook/react": "5.3.7",
42 | "chalk": "3.0.0",
43 | "cross-env": "5.2.0",
44 | "husky": "3.1.0",
45 | "jest": "24.9.0",
46 | "lerna": "3.16.4",
47 | "lint-staged": "9.5.0",
48 | "prettier": "1.19.1",
49 | "rimraf": "3.0.0",
50 | "rollup": "1.27.5",
51 | "rollup-plugin-node-resolve": "5.2.0",
52 | "rollup-plugin-replace": "2.2.0",
53 | "rollup-plugin-typescript": "1.0.1",
54 | "storybook": "5.3.7",
55 | "ts-jest": "24.2.0",
56 | "tslib": "1.10.0",
57 | "tslint": "5.19.0",
58 | "tslint-config-prettier": "1.18.0",
59 | "typescript": "3.7.2"
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Artem Zakharchenko
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/packages/atomic-layout-core/global.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Global flag that indicates production environment.
3 | */
4 | declare const __PROD__: string
5 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | name: 'Atomic Layout core',
3 | verbose: true,
4 | roots: ['src'],
5 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
6 | testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$',
7 | transform: {
8 | '^.+\\.tsx?$': 'ts-jest',
9 | },
10 | }
11 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/jest.setup.js:
--------------------------------------------------------------------------------
1 | // Perform all unit tests in production environment
2 | global.__PROD__ = true
3 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@atomic-layout/core",
3 | "description": "Atomic Layout core module",
4 | "version": "0.14.1",
5 | "license": "MIT",
6 | "main": "lib/index.js",
7 | "typings": "lib/index.d.ts",
8 | "scripts": {
9 | "start": "rollup -cw",
10 | "lint": "tslint 'src/**/*.ts'",
11 | "test": "jest --runInBand",
12 | "clean": "rimraf lib",
13 | "build:types": "ttsc --skipLibCheck",
14 | "build": "cross-env NODE_ENV=production rollup -c",
15 | "verify": "yarn lint && yarn test && yarn build",
16 | "prepublishOnly": "yarn verify"
17 | },
18 | "devDependencies": {
19 | "@types/jest": "24.0.23",
20 | "@types/match-media-mock": "^0.1.5",
21 | "@types/node": "12.12.14",
22 | "cross-env": "5.2.0",
23 | "jest": "24.9.0",
24 | "match-media-mock": "0.1.1",
25 | "react": "16.12.0",
26 | "typescript": "3.7.2"
27 | },
28 | "files": [
29 | "lib"
30 | ],
31 | "publishConfig": {
32 | "access": "public"
33 | },
34 | "author": {
35 | "name": "Artem Zakharchenko",
36 | "email": "kettanaito@gmail.com"
37 | },
38 | "repository": {
39 | "type": "git",
40 | "url": "https://github.com/kettanaito/atomic-layout/tree/master/packages/atomic-layout-core"
41 | },
42 | "bugs": {
43 | "url": "https://github.com/kettanaito/atomic-layout/issues"
44 | },
45 | "funding": {
46 | "type": "opencollective",
47 | "url": "https://opencollective.com/atomic-layout"
48 | },
49 | "keywords": [
50 | "atomic",
51 | "layout",
52 | "core"
53 | ]
54 | }
55 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/rollup.config.js:
--------------------------------------------------------------------------------
1 | import path from 'path'
2 | import replace from 'rollup-plugin-replace'
3 | import resolve from 'rollup-plugin-node-resolve'
4 | import commonjs from 'rollup-plugin-commonjs'
5 | import typescript from 'rollup-plugin-typescript2'
6 | import packageJson from './package.json'
7 |
8 | const { NODE_ENV: nodeEnv } = process.env
9 | const PRODUCTION = nodeEnv === 'production'
10 | const __PROD__ = PRODUCTION ? 'true' : '""'
11 |
12 | export default {
13 | input: 'src/index.ts',
14 | output: {
15 | file: path.resolve(__dirname, packageJson.main),
16 | format: 'cjs',
17 | exports: 'named',
18 | },
19 | plugins: [
20 | resolve({
21 | mainFields: ['esnext'],
22 | extensions: ['.ts'],
23 | }),
24 | typescript(),
25 | commonjs(),
26 | replace({
27 | __PROD__,
28 | }),
29 | ],
30 | }
31 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/Layout.ts:
--------------------------------------------------------------------------------
1 | import defaultOptions, {
2 | LayoutOptions,
3 | Breakpoints,
4 | MeasurementUnit,
5 | BreakpointBehavior,
6 | } from './const/defaultOptions'
7 | import invariant from './utils/functions/invariant'
8 | import warn from './utils/functions/warn'
9 |
10 | class Layout {
11 | public defaultUnit: MeasurementUnit = defaultOptions.defaultUnit
12 | public defaultBehavior: BreakpointBehavior = defaultOptions.defaultBehavior
13 | public breakpoints: Breakpoints = defaultOptions.breakpoints
14 | public defaultBreakpointName: string = defaultOptions.defaultBreakpointName
15 | protected isConfigureCalled: boolean = false
16 |
17 | constructor(options?: Partial) {
18 | return options ? this.configure(options, false) : this
19 | }
20 |
21 | /**
22 | * Applies global layout options.
23 | * Make sure to call this method once, preferably on the rool level
24 | * of your application.
25 | */
26 | public configure(options: Partial, warnOnMultiple = true) {
27 | if (warnOnMultiple) {
28 | warn(
29 | !this.isConfigureCalled,
30 | 'Failed to configure Layout: do not call `Layout.configure()` more than once. Layout configuration must remain consistent throughout the application.',
31 | )
32 | }
33 |
34 | invariant(
35 | options && typeof options === 'object',
36 | `Failed to configure Layout: expected an options Object, but got: ${options}.`,
37 | )
38 |
39 | Object.keys(options || {}).forEach((optionName: keyof LayoutOptions) => {
40 | ;(this[optionName] as any) = options[optionName]
41 | })
42 |
43 | invariant(
44 | this.breakpoints,
45 | 'Failed to configure Layout: expected to have at least one breakpoint specified, but got none.',
46 | )
47 |
48 | invariant(
49 | this.breakpoints.hasOwnProperty(this.defaultBreakpointName),
50 | `Failed to configure Layout: cannot use "${this.defaultBreakpointName}" as the default breakpoint (breakpoint not found).`,
51 | )
52 |
53 | invariant(
54 | this.defaultBreakpointName,
55 | `Failed to configure Layout: expected "defaultBreakpointName" property set, but got: ${this.defaultBreakpointName}.`,
56 | )
57 |
58 | // Mark configure method as called to prevent its multiple calls
59 | this.isConfigureCalled = warnOnMultiple
60 |
61 | return this
62 | }
63 | }
64 |
65 | export default new Layout()
66 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/const/defaultOptions.ts:
--------------------------------------------------------------------------------
1 | type AbsoluteUnits = 'cm' | 'mm' | 'in' | 'px' | 'pt' | 'pc'
2 | type RelativeUnits =
3 | | '%'
4 | | 'em'
5 | | 'ex'
6 | | 'ch'
7 | | 'rem'
8 | | 'vw'
9 | | 'vh'
10 | | 'vmin'
11 | | 'vmax'
12 |
13 | export type Numeric = number | string
14 | export type MeasurementUnit = AbsoluteUnits | RelativeUnits
15 | export type BreakpointBehavior = 'up' | 'down' | 'only'
16 | export interface Breakpoints {
17 | [breakpointName: string]: Breakpoint
18 | }
19 | export interface LayoutOptions {
20 | /**
21 | * Measurement unit that suffixes numeric prop values.
22 | * @default "px"
23 | * @example
24 | *
25 | * // "padding: 10px"
26 | */
27 | defaultUnit: MeasurementUnit
28 | /**
29 | * Map of layout breakpoints.
30 | */
31 | breakpoints: Breakpoints
32 | /**
33 | * Breakpoint name to use when no explicit breakpoint
34 | * name is specified in a prop name.
35 | * @default "xs"
36 | */
37 | defaultBreakpointName: string
38 | defaultBehavior: BreakpointBehavior
39 | }
40 |
41 | export interface MediaQuery {
42 | minHeight?: Numeric
43 | maxHeight?: Numeric
44 | minWidth?: Numeric
45 | maxWidth?: Numeric
46 | minResolution?: string
47 | maxResolution?: string
48 | aspectRatio?: string
49 | minAspectRatio?: string
50 | maxAspectRatio?: string
51 | scan?: 'interlace' | 'progressive'
52 | orientation?: 'portrait' | 'landscape'
53 | displayMode?: 'fullscreen' | 'standalone' | 'minimal-ui' | 'browser'
54 | }
55 |
56 | export interface Breakpoint extends MediaQuery {
57 | /* Index signature for dynamic breakpoint composition */
58 | [propName: string]: any
59 | }
60 |
61 | const defaultOptions: LayoutOptions = {
62 | defaultUnit: 'px',
63 | defaultBehavior: 'up',
64 | defaultBreakpointName: 'xs',
65 | breakpoints: {
66 | xs: {
67 | maxWidth: '575px',
68 | },
69 | sm: {
70 | minWidth: '576px',
71 | maxWidth: '767px',
72 | },
73 | md: {
74 | minWidth: '768px',
75 | maxWidth: '991px',
76 | },
77 | lg: {
78 | minWidth: '992px',
79 | maxWidth: '1199px',
80 | },
81 | xl: {
82 | minWidth: '1200px',
83 | },
84 | },
85 | }
86 |
87 | export default defaultOptions
88 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/index.ts:
--------------------------------------------------------------------------------
1 | export { default as Layout } from './Layout'
2 | export { default as defaultOptions } from './const/defaultOptions'
3 |
4 | /* Typings */
5 | export {
6 | Numeric,
7 | MediaQuery,
8 | Breakpoint,
9 | Breakpoints,
10 | BreakpointBehavior,
11 | } from './const/defaultOptions'
12 | export {
13 | GenericProps,
14 | GridProps,
15 | BoxProps,
16 | CompositionProps,
17 | CompositionRenderProp,
18 | } from './const/props'
19 |
20 | /* Styles */
21 | export { default as applyStyles } from './utils/styles/applyStyles'
22 | export {
23 | default as createMediaQuery,
24 | joinQueryList,
25 | } from './utils/styles/createMediaQuery'
26 | export { default as normalizeQuery } from './utils/styles/normalizeQuery'
27 |
28 | /* Breakpoints */
29 | export { default as withBreakpoints } from './utils/breakpoints/withBreakpoints'
30 | export { default as openBreakpoint } from './utils/breakpoints/openBreakpoint'
31 | export { default as closeBreakpoint } from './utils/breakpoints/closeBreakpoint'
32 | export { default as mergeAreaRecords } from './utils/breakpoints/mergeAreaRecords'
33 |
34 | /* Props */
35 | export {
36 | default as propAliases,
37 | PropAliases,
38 | PropAliasDeclaration,
39 | } from './const/propAliases'
40 | export {
41 | default as parsePropName,
42 | ParsedProp,
43 | ParsedBreakpoint,
44 | } from './utils/strings/parsePropName'
45 | export { default as parseTemplates } from './utils/templates/parseTemplates'
46 | export {
47 | default as generateComponents,
48 | AreasMap,
49 | AreaComponent,
50 | } from './utils/templates/generateComponents'
51 |
52 | /* Utilities */
53 | export { default as warn } from './utils/functions/warn'
54 | export { default as compose } from './utils/functions/compose'
55 | export { default as throttle } from './utils/functions/throttle'
56 | export { default as transformNumeric } from './utils/math/transformNumeric'
57 | export { default as memoizeWith } from './utils/functions/memoizeWith'
58 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/breakpoints/closeBreakpoint/closeBreakpoint.spec.ts:
--------------------------------------------------------------------------------
1 | import closeBreakpoint from './closeBreakpoint'
2 |
3 | describe('closeBreakpoint', () => {
4 | describe('given numeric breakpoints', () => {
5 | const breakpoint = closeBreakpoint({
6 | minHeight: 350,
7 | maxHeight: 700,
8 | minWidth: 500,
9 | maxWidth: 621,
10 | })
11 |
12 | it('should not contain original "min-" properties', () => {
13 | expect(breakpoint).not.toContain(['minHeight', 'minWidth'])
14 | })
15 |
16 | it('should coerce "max-" properties into {min} - 1', () => {
17 | expect(breakpoint).toHaveProperty('maxHeight', 349)
18 | expect(breakpoint).toHaveProperty('maxWidth', 499)
19 | })
20 | })
21 |
22 | describe('given custom measurement unit', () => {
23 | const breakpoint = closeBreakpoint({
24 | minHeight: '350px',
25 | maxHeight: '700px',
26 | minWidth: '500px',
27 | maxWidth: '621px',
28 | })
29 |
30 | it('should not contain original "min-" properties', () => {
31 | expect(breakpoint).not.toContain(['minHeight', 'minWidth'])
32 | })
33 |
34 | it('should coerce "max-" properties into {min} - 1 + unit', () => {
35 | expect(breakpoint).toHaveProperty('maxHeight', '349px')
36 | expect(breakpoint).toHaveProperty('maxWidth', '499px')
37 | })
38 | })
39 | })
40 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/breakpoints/closeBreakpoint/closeBreakpoint.ts:
--------------------------------------------------------------------------------
1 | import { Breakpoint } from '../../../const/defaultOptions'
2 | import getPrefix from '../../strings/getPrefix'
3 |
4 | /**
5 | * Accepts a breakpoint and returns a new breakpoint where
6 | * all the "min" properties of the original breakpoint are
7 | * flipped into the "max" properties. Any "max" properties
8 | * of the original breakpoint are omitted.
9 | * Subtracts 1 from the numeric values of all "min" properties
10 | * to not overlap with the given breakpoint.
11 | *
12 | * @example
13 | * flipBreakpoint({ minWidth: 500, maxWidth: 600 })
14 | * // { maxWidth: 499 }
15 | */
16 | export default function flipBreakpoint(breakpoint: Breakpoint): Breakpoint {
17 | return Object.entries(breakpoint)
18 | .map<[string, string, any]>(([propName, propValue]) => [
19 | getPrefix(propName),
20 | propName,
21 | propValue,
22 | ])
23 | .filter(([prefix]) => prefix !== 'max')
24 | .reduce((newBreakpoint, [prefix, propName, propValue]) => {
25 | const hasMinPrefix = prefix === 'min'
26 | const nextPropName = hasMinPrefix
27 | ? propName.replace(/^min/, 'max')
28 | : propName
29 |
30 | // Parse a breakpoint property value into a numeric value
31 | // and its measurement unit.
32 | const [, numericValue, unit] = /(\d+)(.+)?/.exec(propValue)
33 |
34 | /**
35 | * Subtracts 1 from the edge to not include the area at the beginning
36 | * of the breakpoint.
37 | *
38 | * @todo
39 | * How is "parseFloat" going to work with non-dimensional options?
40 | * (i.e. aspectRatio)
41 | */
42 | const nextNumericValue = hasMinPrefix
43 | ? parseFloat(numericValue) - 1
44 | : numericValue
45 |
46 | // Append back the measurement unit.
47 | // Prevents breakpoints like { "minWidth": "768px" }
48 | // becoming { "minWidth": 768 } -> { "minWidth": "768rem" }
49 | // when the measurement unit is not "px".
50 | const nextValue = unit ? `${nextNumericValue}${unit}` : nextNumericValue
51 |
52 | return {
53 | ...newBreakpoint,
54 | [nextPropName]: nextValue,
55 | }
56 | }, {})
57 | }
58 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/breakpoints/closeBreakpoint/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './closeBreakpoint'
2 | export * from './closeBreakpoint'
3 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/breakpoints/getAreaRecords/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './getAreaRecords'
2 | export * from './getAreaRecords'
3 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/breakpoints/mergeAreaRecords/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './mergeAreaRecords'
2 | export * from './mergeAreaRecords'
3 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/breakpoints/mergeAreaRecords/mergeAreaRecords.ts:
--------------------------------------------------------------------------------
1 | import { AreaRecord } from '../getAreaRecords'
2 | import { Breakpoint } from '../../../const/defaultOptions'
3 | import transformNumeric from '../../math/transformNumeric'
4 | import getPrefix from '../../strings/getPrefix'
5 |
6 | /**
7 | * Merges two given area records.
8 | */
9 | export default function mergeAreaRecords(
10 | nextAreaRecord: AreaRecord,
11 | prevAreaRecord: AreaRecord,
12 | includesArea: boolean,
13 | ): AreaRecord {
14 | const { behavior: prevRecordBehavior } = prevAreaRecord
15 | const { behavior: nextRecordBehavior } = nextAreaRecord
16 |
17 | const wentUp = prevRecordBehavior === 'up'
18 | const goesDown = nextRecordBehavior === 'down'
19 | const behavesSame = prevRecordBehavior === nextRecordBehavior
20 | const behavesInclusive = wentUp && goesDown
21 | const shouldStretch = wentUp
22 |
23 | const nextBehavior =
24 | !includesArea && shouldStretch ? 'down' : nextRecordBehavior
25 |
26 | const mergedBreakpoint = {
27 | ...prevAreaRecord.breakpoint,
28 | ...nextAreaRecord.breakpoint,
29 | }
30 |
31 | const nextBreakpoint = Object.keys(mergedBreakpoint).reduce(
32 | (acc, propName) => {
33 | let nextValue = mergedBreakpoint[propName]
34 | const prefix = getPrefix(propName)
35 |
36 | if (prefix === 'max') {
37 | if (!includesArea && shouldStretch) {
38 | const reversedValue =
39 | nextAreaRecord.breakpoint[propName.replace(/^max/, 'min')]
40 | nextValue = `calc(${transformNumeric(reversedValue)} - 1px)`
41 | }
42 | }
43 |
44 | if (prefix === 'min') {
45 | if (includesArea) {
46 | if (behavesSame || behavesInclusive) {
47 | nextValue = prevAreaRecord.breakpoint[propName]
48 | }
49 | } else {
50 | if (shouldStretch) {
51 | nextValue = prevAreaRecord.breakpoint[propName]
52 | }
53 | }
54 | }
55 |
56 | return {
57 | ...acc,
58 | [propName]: nextValue,
59 | }
60 | },
61 | {},
62 | )
63 |
64 | return {
65 | behavior: nextBehavior,
66 | breakpoint: nextBreakpoint,
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/breakpoints/openBreakpoint/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './openBreakpoint'
2 | export * from './openBreakpoint'
3 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/breakpoints/openBreakpoint/openBreakpoint.spec.ts:
--------------------------------------------------------------------------------
1 | import { Breakpoint } from '../../../const/defaultOptions'
2 | import openBreakpoint from './openBreakpoint'
3 |
4 | describe('openBreakpoint', () => {
5 | describe('given already open breakpoint', () => {
6 | const breakpoint = {
7 | minHeight: 500,
8 | }
9 |
10 | it('should return breakpoint as-is', () => {
11 | expect(openBreakpoint(breakpoint)).toEqual({
12 | minHeight: 500,
13 | })
14 | })
15 | })
16 |
17 | describe('given a closed breakpoint', () => {
18 | const breakpoint = {
19 | maxWidth: 768,
20 | }
21 |
22 | it('should return an empty breakpoint', () => {
23 | expect(openBreakpoint(breakpoint)).toEqual({})
24 | })
25 | })
26 |
27 | describe('given an inclusive breakpoint', () => {
28 | let result: Breakpoint
29 | const breakpoint = {
30 | minResolution: '150dpi',
31 | maxResolution: '300dpi',
32 | }
33 |
34 | beforeAll(() => {
35 | result = openBreakpoint(breakpoint)
36 | })
37 |
38 | it('should set "maxResolution" to undefined', () => {
39 | expect(result).toHaveProperty('maxResolution', undefined)
40 | })
41 |
42 | it('should preserve "minResolution" value', () => {
43 | expect(result).toHaveProperty('minResolution', '150dpi')
44 | })
45 | })
46 |
47 | describe('given a complex breakpoint', () => {
48 | let result: Breakpoint
49 | const breakpoint = {
50 | minWidth: 300,
51 | maxWidth: 500,
52 | minHeight: 200,
53 | maxHeight: 400,
54 | minResolution: '150dpi',
55 | maxResolution: '300dpi',
56 | maxAspectRatio: '1/3',
57 | }
58 |
59 | beforeAll(() => {
60 | result = openBreakpoint(breakpoint)
61 | })
62 |
63 | it('should set all "max-" properties to undefined', () => {
64 | expect(result).toHaveProperty('maxWidth', undefined)
65 | expect(result).toHaveProperty('maxHeight', undefined)
66 | expect(result).toHaveProperty('maxResolution', undefined)
67 | expect(result).toHaveProperty('maxAspectRatio', undefined)
68 | })
69 |
70 | it('should preserve all "min-" properties', () => {
71 | expect(result).toHaveProperty('minWidth', 300)
72 | expect(result).toHaveProperty('minHeight', 200)
73 | expect(result).toHaveProperty('minResolution', '150dpi')
74 | })
75 | })
76 | })
77 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/breakpoints/openBreakpoint/openBreakpoint.ts:
--------------------------------------------------------------------------------
1 | import { Breakpoint } from '../../../const/defaultOptions'
2 | import getPrefix from '../../strings/getPrefix'
3 |
4 | /**
5 | * Opens the given breakpoint.
6 | * A breakpoint is considered open when it has no upper boundary. For example,
7 | * a breakpoint that has "maxWidth: undefined" is the open breakpoint.
8 | */
9 | export default function openBreakpoint(
10 | breakpoint: Breakpoint,
11 | ): T {
12 | return Object.keys(breakpoint).reduce(
13 | (acc, key) => ({
14 | ...acc,
15 | [key]: getPrefix(key) === 'max' ? undefined : breakpoint[key],
16 | }),
17 | {} as T,
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/breakpoints/shouldMergeBreakpoints/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './shouldMergeBreakpoints'
2 | export * from './shouldMergeBreakpoints'
3 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/breakpoints/shouldMergeBreakpoints/shouldMergeBreakpoints.spec.ts:
--------------------------------------------------------------------------------
1 | import shouldMergeBreakpoints from './shouldMergeBreakpoints'
2 |
3 | describe('shouldMergeBreakpoints', () => {
4 | it('returns "true" for combinable breakpoints', () => {
5 | expect(
6 | shouldMergeBreakpoints(
7 | {
8 | maxWidth: 576,
9 | },
10 | {
11 | minWidth: 580,
12 | maxWidth: 750,
13 | },
14 | ),
15 | ).toBe(true)
16 |
17 | expect(
18 | shouldMergeBreakpoints(
19 | {
20 | minResolution: '70dpi',
21 | },
22 | {
23 | minResolution: '320dpi',
24 | },
25 | ),
26 | ).toBe(true)
27 |
28 | expect(
29 | shouldMergeBreakpoints(
30 | {
31 | height: 200,
32 | },
33 | {
34 | height: 500,
35 | },
36 | ),
37 | ).toBe(true)
38 |
39 | expect(
40 | shouldMergeBreakpoints(
41 | {
42 | minAspectRatio: '3/4',
43 | },
44 | {
45 | minAspectRatio: '3/4',
46 | maxAspectRatio: '16/9',
47 | },
48 | ),
49 | ).toBe(true)
50 | })
51 |
52 | it('returns "false" for non-combinable breakpoints', () => {
53 | expect(
54 | shouldMergeBreakpoints(
55 | {
56 | maxWidth: 500,
57 | },
58 | {
59 | width: 600,
60 | },
61 | ),
62 | ).toBe(false)
63 |
64 | expect(
65 | shouldMergeBreakpoints(
66 | {
67 | resolution: '300dpi',
68 | },
69 | {
70 | maxResolution: '300dpi',
71 | },
72 | ),
73 | ).toBe(false)
74 | })
75 |
76 | it('accounts property names with equal length', () => {
77 | expect(
78 | shouldMergeBreakpoints(
79 | {
80 | minFooBar: 1,
81 | },
82 | {
83 | minFooBar: 2,
84 | maxDoeBar: 3,
85 | },
86 | ),
87 | ).toBe(false)
88 | })
89 | })
90 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/breakpoints/shouldMergeBreakpoints/shouldMergeBreakpoints.ts:
--------------------------------------------------------------------------------
1 | import { Breakpoint } from '../../../const/defaultOptions'
2 |
3 | /**
4 | * Replaces the prefixes in a parameter name.
5 | * Allows strict comparison of same parameters with different prefixes.
6 | * Does not test for inclusion/notch.
7 | *
8 | * @example
9 | * neutralizeParamName('maxWidth') // "_width"
10 | * neutralizeParamName('minWidth') // "_width"
11 | */
12 | const neutralizeParamName = (paramName: string): string => {
13 | return paramName.replace(/^min|max/, '_')
14 | }
15 |
16 | /**
17 | * Determines whether two given breakpoints can be merged.
18 | * Assures non-compatible breakpoints are not prompted to
19 | * be merged during the area params composition.
20 | */
21 | export default function shouldCombineBreakpoints(
22 | breakpointA: Breakpoint,
23 | breakpointB: Breakpoint,
24 | ): boolean {
25 | const allParams = Object.keys(breakpointA).concat(Object.keys(breakpointB))
26 |
27 | return allParams.every((pristineParamName, index) => {
28 | const paramName = neutralizeParamName(pristineParamName)
29 | const prevParamName = neutralizeParamName(allParams[index - 1] || paramName)
30 |
31 | return paramName === prevParamName
32 | })
33 | }
34 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/breakpoints/withBreakpoints/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './withBreakpoints'
2 | export * from './withBreakpoints'
3 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/breakpoints/withBreakpoints/matchMedia.mock.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable-rule no-implicit-dependencies */
2 | import matchMediaMock from 'match-media-mock'
3 |
4 | const mock = matchMediaMock.create()
5 |
6 | mock.setConfig({
7 | type: 'screen',
8 | height: window.innerHeight,
9 | width: window.innerWidth,
10 | })
11 |
12 | window.matchMedia = mock
13 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/breakpoints/withBreakpoints/withBreakpoints.spec.ts:
--------------------------------------------------------------------------------
1 | import './matchMedia.mock'
2 | import withBreakpoints from './withBreakpoints'
3 |
4 | describe('withBreakpoints', () => {
5 | describe('given existing breakpoints', () => {
6 | it('should return value associated with matching breakpoint', () => {
7 | expect(
8 | withBreakpoints({
9 | xs: 'extra small',
10 | md: 'medium',
11 | lg: 'large',
12 | }),
13 | ).toEqual('large')
14 | })
15 |
16 | it('should return the default value when no breakpoints match', () => {
17 | expect(
18 | withBreakpoints(
19 | {
20 | md: 'medium',
21 | },
22 | 'default',
23 | ),
24 | ).toEqual('default')
25 | })
26 |
27 | it('should return "undefined" when not given default value', () => {
28 | expect(
29 | withBreakpoints({
30 | md: 'medium',
31 | }),
32 | ).toBeUndefined()
33 | })
34 | })
35 |
36 | describe('given non-existing breakpoints', () => {
37 | it('should print console warning', () => {
38 | const consoleSpy = jest.spyOn(console, 'warn')
39 | withBreakpoints({
40 | foo: 'bar',
41 | })
42 | expect(console.warn).toHaveBeenCalledTimes(1)
43 | consoleSpy.mockRestore()
44 | })
45 |
46 | it('should return default value when given', () => {
47 | expect(
48 | withBreakpoints(
49 | {
50 | foo: 'bar',
51 | },
52 | 'default',
53 | ),
54 | ).toEqual('default')
55 | })
56 |
57 | it('should return "undefined" when not given default value', () => {
58 | expect(
59 | withBreakpoints({
60 | foo: 'bar',
61 | }),
62 | ).toBeUndefined()
63 | })
64 | })
65 | })
66 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/breakpoints/withBreakpoints/withBreakpoints.ts:
--------------------------------------------------------------------------------
1 | import Layout from '../../../Layout'
2 | import { Breakpoints } from '../../../const/defaultOptions'
3 | import createMediaQuery from '../../styles/createMediaQuery'
4 |
5 | const createWithBreakpoints = (breakpoints: Breakpoints) => {
6 | const existingBreakpoints = Object.keys(breakpoints)
7 |
8 | return (breakpointsMap: Record, defaultValue?: T): T => {
9 | const breakpointMatch = Object.keys(breakpointsMap)
10 | .filter((breakpointName) => {
11 | const hasBreakpoint = existingBreakpoints.includes(breakpointName)
12 |
13 | if (!hasBreakpoint) {
14 | console.warn(
15 | 'useBreakpoints: Breakpoint "%s" is not found. Add it via "Layout.configure()", or use one of the existing breakpoints (%s).',
16 | breakpointName,
17 | existingBreakpoints.join(', '),
18 | )
19 | }
20 |
21 | return hasBreakpoint
22 | })
23 | .find((breakpointName) => {
24 | const breakpoint: Breakpoints | undefined = breakpoints[breakpointName]
25 | const mediaQuery = createMediaQuery(breakpoint, 'only')
26 | return breakpoint && matchMedia(mediaQuery).matches
27 | })
28 |
29 | return breakpointMatch ? breakpointsMap[breakpointMatch] : defaultValue
30 | }
31 | }
32 |
33 | const withBreakpoints = createWithBreakpoints(Layout.breakpoints)
34 |
35 | export default withBreakpoints
36 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/functions/compose/compose.spec.ts:
--------------------------------------------------------------------------------
1 | import compose from './compose'
2 |
3 | describe('compose', () => {
4 | it('composes list of functions from right to left', () => {
5 | const func = compose(
6 | (arr: string[]) => arr.reverse(),
7 | (str: string) => str.split(' '),
8 | (str: string) => str.toUpperCase(),
9 | )
10 |
11 | const funcRes = func('Lorem ipsum')
12 | expect(funcRes).toEqual(['IPSUM', 'LOREM'])
13 | })
14 | })
15 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/functions/compose/compose.ts:
--------------------------------------------------------------------------------
1 | type Func = (...args: any[]) => any
2 |
3 | /**
4 | * Returns a functional composition of the given functions.
5 | * Applies no currying.
6 | */
7 | export default function compose(...funcs: Func[]) {
8 | return funcs.reduce((f, g) => (...args: any[]) => f(g(...args)))
9 | }
10 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/functions/compose/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './compose'
2 | export * from './compose'
3 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/functions/invariant/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './invariant'
2 | export * from './invariant'
3 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/functions/invariant/invariant.spec.ts:
--------------------------------------------------------------------------------
1 | import invariant from './invariant'
2 |
3 | describe('invariant', () => {
4 | describe('given predicate is satisfied', () => {
5 | it('should not throw any errors', () => {
6 | const run = () => invariant(true, 'You should not see this')
7 | expect(run).not.toThrow()
8 | })
9 | })
10 |
11 | describe('given predicate is not satisfied', () => {
12 | it('should throw an error with the correct message', () => {
13 | const errorMessage = 'Error message'
14 | const run = () => invariant(false, errorMessage)
15 |
16 | expect(run).toThrowError(errorMessage)
17 | })
18 | })
19 | })
20 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/functions/invariant/invariant.ts:
--------------------------------------------------------------------------------
1 | export default function invariant(variable: any, message: string): void {
2 | if (!variable) {
3 | throw new Error(message)
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/functions/isset/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './isset'
2 | export * from './isset'
3 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/functions/isset/isset.spec.ts:
--------------------------------------------------------------------------------
1 | import isset from './isset'
2 |
3 | describe('isset', () => {
4 | describe('given a set variable', () => {
5 | it('should return true', () => {
6 | expect(isset(0)).toBe(true)
7 | expect(isset('')).toBe(true)
8 | expect(isset('a')).toBe(true)
9 | expect(isset([])).toBe(true)
10 | expect(isset({})).toBe(true)
11 | expect(isset(() => 'foo')).toBe(true)
12 | })
13 | })
14 |
15 | describe('given null', () => {
16 | it('should return false', () => {
17 | expect(isset(null)).toBe(false)
18 | })
19 | })
20 |
21 | describe('given undefined', () => {
22 | it('should return false', () => {
23 | expect(isset(undefined)).toBe(false)
24 | })
25 | })
26 | })
27 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/functions/isset/isset.ts:
--------------------------------------------------------------------------------
1 | export default function isset(variable: any): boolean {
2 | return typeof variable !== 'undefined' && variable !== null
3 | }
4 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/functions/memoizeWith/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './memoizeWith'
2 | export * from './memoizeWith'
3 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/functions/memoizeWith/memoizeWith.spec.ts:
--------------------------------------------------------------------------------
1 | import memoizeWith from './memoizeWith'
2 |
3 | const factorial = (num: number) => {
4 | return Array(num)
5 | .fill(0)
6 | .map((_, i) => i + 1)
7 | .reduce((acc, n) => acc * n, 1)
8 | }
9 |
10 | const factorialMock = jest.fn(factorial)
11 |
12 | describe('memoizeWith', () => {
13 | describe('given memoized a function', () => {
14 | let memoizedFactorial: typeof factorial
15 |
16 | beforeAll(() => {
17 | const memoizeWithIdentity = memoizeWith((n) =>
18 | String(n),
19 | )
20 | memoizedFactorial = memoizeWithIdentity(factorialMock)
21 | })
22 |
23 | afterEach(() => {
24 | factorialMock.mockClear()
25 | })
26 |
27 | it('should return the result when called', () => {
28 | expect(memoizedFactorial(5)).toEqual(120)
29 | expect(factorialMock).toBeCalledTimes(1)
30 | })
31 |
32 | it('should return memoized result when given the same arguments', () => {
33 | memoizedFactorial(5)
34 | memoizedFactorial(5)
35 | memoizedFactorial(5)
36 | expect(factorialMock).toBeCalledTimes(0)
37 | })
38 |
39 | it('should execute anew for each unique set of arguments', () => {
40 | memoizedFactorial(3)
41 | memoizedFactorial(4)
42 | expect(factorialMock).toBeCalledTimes(2)
43 | })
44 | })
45 | })
46 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/functions/memoizeWith/memoizeWith.ts:
--------------------------------------------------------------------------------
1 | const memoizeWith = any>(
2 | saltGenerator: (...args: Parameters) => string,
3 | ) => {
4 | const cache: Record = {}
5 |
6 | return (func: F) =>
7 | function(...args: Parameters): ReturnType {
8 | const key = saltGenerator(...args)
9 |
10 | if (!(key in cache)) {
11 | cache[key] = func(...args)
12 | }
13 |
14 | return cache[key]
15 | }
16 | }
17 |
18 | export default memoizeWith
19 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/functions/pop/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './pop'
2 | export * from './pop'
3 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/functions/pop/pop.spec.ts:
--------------------------------------------------------------------------------
1 | import pop from '../pop'
2 |
3 | describe('pop', () => {
4 | describe('given an empty array', () => {
5 | const array: any[] = []
6 | let result: string[]
7 |
8 | beforeAll(() => {
9 | result = pop(array)
10 | })
11 |
12 | it('should return a new empty array', () => {
13 | expect(result).toEqual([])
14 | })
15 |
16 | it('should not mutate the original array', () => {
17 | expect(array).toEqual([])
18 | })
19 | })
20 |
21 | describe('given an array with a single member', () => {
22 | const array = ['area']
23 | let result: string[]
24 |
25 | beforeAll(() => {
26 | result = pop(array)
27 | })
28 |
29 | it('should return a new array with the last member removed', () => {
30 | expect(result).toEqual([])
31 | })
32 |
33 | it('should not mutate the original array', () => {
34 | expect(array).toEqual(['area'])
35 | })
36 | })
37 |
38 | describe('given an array with multiple members', () => {
39 | const array = [1, 2, 3]
40 | let result: number[]
41 |
42 | beforeAll(() => {
43 | result = pop(array)
44 | })
45 |
46 | it('should return a new array with the last member removed', () => {
47 | expect(result).toEqual([1, 2])
48 | })
49 |
50 | it('should not mutate the original array', () => {
51 | expect(array).toEqual([1, 2, 3])
52 | })
53 | })
54 | })
55 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/functions/pop/pop.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Returns the shallow copy of the given array with
3 | * the last element removed.
4 | */
5 | export default function pop(list: T[]): T[] {
6 | return list.slice(0, list.length - 1)
7 | }
8 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/functions/throttle/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './throttle'
2 | export * from './throttle'
3 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/functions/throttle/throttle.spec.ts:
--------------------------------------------------------------------------------
1 | import throttle from './throttle'
2 |
3 | describe('throttle', () => {
4 | let payload: number = 0
5 |
6 | afterEach(() => {
7 | payload = 0
8 | })
9 |
10 | const func = (amount: number = 1) => {
11 | payload += amount
12 | }
13 | const throttledFunc = throttle(func, 50)
14 |
15 | it('should not be called more than once per interval', (done) => {
16 | throttledFunc() // 1 (leading)
17 | throttledFunc() // ignore
18 | setTimeout(throttledFunc, 30) // ignore
19 | setTimeout(throttledFunc, 60) // 2
20 | setTimeout(throttledFunc, 70) // 3 (trailing)
21 |
22 | setTimeout(
23 | () => {
24 | expect(payload).toBe(3)
25 | done()
26 | },
27 | // Await for last call timestamp + interval
28 | // for the trailing function call.
29 | 120,
30 | )
31 | })
32 |
33 | it('should preserve original call signature', () => {
34 | throttledFunc(5)
35 | setTimeout(() => {
36 | expect(payload).toBe(5)
37 | }, 50)
38 | })
39 | })
40 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/functions/throttle/throttle.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Throttles a given function.
3 | * Implements both leading and trailing function calls.
4 | */
5 | export default function throttle any>(
6 | func: F,
7 | interval: number,
8 | ) {
9 | let previous: number
10 | let queuedToRun: NodeJS.Timeout = null
11 |
12 | return function invoker(...args: Parameters): void {
13 | const now = Date.now()
14 | clearTimeout(queuedToRun)
15 |
16 | if (!previous || now - previous >= interval) {
17 | func.apply(null, args)
18 | previous = now
19 | } else {
20 | queuedToRun = setTimeout(
21 | invoker.bind(null, ...args),
22 | interval - (now - previous),
23 | )
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/functions/warn/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './warn'
2 | export * from './warn'
3 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/functions/warn/warn.spec.ts:
--------------------------------------------------------------------------------
1 | import warn from './warn'
2 |
3 | describe('warn', () => {
4 | describe('warns when not satisfied predicate', () => {
5 | const values = [
6 | ['0', 0],
7 | ['false', false],
8 | ['null', null],
9 | ['undefined', undefined],
10 | ]
11 |
12 | values.forEach(([name, value]) => {
13 | it(`when given ${name}`, () => {
14 | const consoleSpy = jest.spyOn(console, 'warn')
15 | const message = 'Warning message'
16 |
17 | warn(value, message)
18 | expect(consoleSpy).toBeCalledTimes(1)
19 | expect(consoleSpy).toBeCalledWith(message)
20 |
21 | consoleSpy.mockRestore()
22 | })
23 | })
24 | })
25 |
26 | describe('does nothing when satisfies predicate', () => {
27 | const values = [
28 | ['one', 1],
29 | ['true', true],
30 | ['object', {}],
31 | ['array', [] as any[]],
32 | ]
33 |
34 | values.forEach(([name, value]) => {
35 | it(`when given ${name}`, () => {
36 | const consoleSpy = jest.spyOn(console, 'warn')
37 |
38 | warn(value, 'Foo')
39 | expect(consoleSpy).not.toBeCalled()
40 |
41 | consoleSpy.mockRestore()
42 | })
43 | })
44 | })
45 | })
46 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/functions/warn/warn.ts:
--------------------------------------------------------------------------------
1 | export default function warn(predicate: any, message: string) {
2 | if (!predicate) {
3 | console.warn(message)
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/math/transformNumeric.spec.ts:
--------------------------------------------------------------------------------
1 | import Layout from '../../Layout'
2 | import transformNumeric from './transformNumeric'
3 |
4 | describe('transformNumeric', () => {
5 | describe('given arbitrary number rather than 0', () => {
6 | describe('and using default measurement unit', () => {
7 | it('should suffix numeric value with the measurement unit', () => {
8 | expect(transformNumeric(5)).toBe('5px')
9 | })
10 | })
11 |
12 | describe('and using custom measurement unit', () => {
13 | beforeAll(() => {
14 | Layout.configure({
15 | defaultUnit: 'rem',
16 | })
17 | })
18 |
19 | it('should suffix numeric value with the custom measurement unit', () => {
20 | expect(transformNumeric(3)).toBe('3rem')
21 | })
22 | })
23 | })
24 |
25 | describe('given a string', () => {
26 | it('should return the string as-is', () => {
27 | expect(transformNumeric('2vh')).toBe('2vh')
28 | })
29 | })
30 |
31 | describe('given a numeric 0', () => {
32 | it('should return explicit "0"', () => {
33 | expect(transformNumeric(0)).toBe('0')
34 | })
35 | })
36 |
37 | describe('given a string zero "0"', () => {
38 | it('should return the string as-is', () => {
39 | expect(transformNumeric('0')).toBe('0')
40 | })
41 | })
42 |
43 | describe('given no input', () => {
44 | it('should return an empty string', () => {
45 | expect(transformNumeric()).toBe('')
46 | expect(transformNumeric('')).toBe('')
47 | })
48 | })
49 | })
50 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/math/transformNumeric.ts:
--------------------------------------------------------------------------------
1 | import Layout from '../../Layout'
2 | import isset from '../functions/isset'
3 |
4 | export default function transformNumeric(value?: number | string): string {
5 | if (!isset(value)) {
6 | return ''
7 | }
8 |
9 | /**
10 | * Suffix numeric value with the default unit.
11 | * Accept explicit (string) value as-is.
12 | *
13 | * When given value is zero then its generated as it is, no suffix is attached
14 | */
15 | const suffix =
16 | typeof value === 'number' && value !== 0 ? Layout.defaultUnit : ''
17 |
18 | return `${value}${suffix}`
19 | }
20 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/strings/capitalize/capitalize.spec.ts:
--------------------------------------------------------------------------------
1 | import capitalize from './capitalize'
2 |
3 | describe('capitalize', () => {
4 | describe('given an arbitrary string', () => {
5 | it('should capitalize the given string', () => {
6 | expect(capitalize('foo')).toBe('Foo')
7 | })
8 | })
9 |
10 | describe('given already capitalized string', () => {
11 | it('should return the string as-is', () => {
12 | expect(capitalize('Foo')).toBe('Foo')
13 | })
14 | })
15 |
16 | describe('given a string with in-string capital letters', () => {
17 | let result: string
18 |
19 | beforeAll(() => {
20 | result = capitalize('fooBarDoe')
21 | })
22 |
23 | it('should capitalize the string', () => {
24 | expect(result).toMatch(/^F/)
25 | })
26 |
27 | it('should preserve existing capital letters', () => {
28 | expect(result).toBe('FooBarDoe')
29 | })
30 | })
31 | })
32 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/strings/capitalize/capitalize.ts:
--------------------------------------------------------------------------------
1 | export default function capitalize(str: string): string {
2 | return str.replace(/^./, (firstLetter) => firstLetter.toUpperCase())
3 | }
4 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/strings/capitalize/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './capitalize'
2 | export * from './capitalize'
3 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/strings/getPrefix/getPrefix.spec.ts:
--------------------------------------------------------------------------------
1 | import getPrefix from './getPrefix'
2 |
3 | describe('getPrefix', () => {
4 | describe('given a string with supported prefix', () => {
5 | it('should return the prefix', () => {
6 | expect(getPrefix('maxHeight')).toBe('max')
7 | expect(getPrefix('minResolution')).toBe('min')
8 | })
9 | })
10 |
11 | describe('given a string with a supported prefix within a string', () => {
12 | it('should ignore in-string matched and return an empty string', () => {
13 | expect(getPrefix('aminmaxWidth')).toBe('')
14 | })
15 | })
16 |
17 | describe('given an arbitrary string', () => {
18 | it('should return an empty string', () => {
19 | expect(getPrefix('fooBar')).toBe('')
20 | expect(getPrefix('abcDef')).toBe('')
21 | })
22 | })
23 | })
24 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/strings/getPrefix/getPrefix.ts:
--------------------------------------------------------------------------------
1 | export default function getPrefix(str: string): string {
2 | const prompt = str.match(/^(min|max)/)
3 | return prompt ? prompt[0] : ''
4 | }
5 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/strings/getPrefix/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './getPrefix'
2 | export * from './getPrefix'
3 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/strings/hashString/hashString.spec.ts:
--------------------------------------------------------------------------------
1 | import hashString from './hashString'
2 |
3 | describe('hashString', () => {
4 | describe('given a string with an arbitrary value', () => {
5 | it('should return a hash based on the given string', () => {
6 | const input = 'template:header,content,footer'
7 |
8 | expect(hashString(input)).toBe(1927731245)
9 | expect(hashString(input)).toBe(1927731245)
10 | expect(hashString('templateMd:header,content,footer')).toBe(1323128868)
11 | })
12 | })
13 |
14 | describe('given an empty string', () => {
15 | it('should return explicit 0', () => {
16 | expect(hashString('')).toBe(0)
17 | })
18 | })
19 | })
20 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/strings/hashString/hashString.ts:
--------------------------------------------------------------------------------
1 | export default function hashString(str: string): number {
2 | const { length } = str
3 | let hash = 0
4 | let i = 0
5 |
6 | if (length > 0) {
7 | while (i < length) {
8 | hash = ((hash << 5) - hash + str.charCodeAt(i++)) | 0
9 | }
10 | }
11 |
12 | return hash
13 | }
14 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/strings/hashString/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './hashString'
2 | export * from './hashString'
3 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/strings/isAreaName/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './isAreaName'
2 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/strings/isAreaName/isAreaName.spec.ts:
--------------------------------------------------------------------------------
1 | import isAreaName from './isAreaName'
2 |
3 | describe('isAreaName', () => {
4 | describe('given a valid string value', () => {
5 | it('should return true', () => {
6 | expect(isAreaName('footer')).toBe(true)
7 | })
8 | })
9 |
10 | describe('given a numeric value', () => {
11 | it('should return false', () => {
12 | expect(isAreaName('100px')).toBe(false)
13 | expect(isAreaName('2fr')).toBe(false)
14 | })
15 | })
16 |
17 | describe('given a reserved keyword', () => {
18 | it('should return false', () => {
19 | expect(isAreaName('/')).toBe(false)
20 | expect(isAreaName('auto')).toBe(false)
21 | })
22 | })
23 |
24 | describe('given a dot placeholder', () => {
25 | describe('given a single dot character', () => {
26 | it('should return false', () => {
27 | expect(isAreaName('.')).toBe(false)
28 | })
29 | })
30 |
31 | describe('given a sequence of dots', () => {
32 | it('should return false', () => {
33 | expect(isAreaName('..')).toBe(false)
34 | expect(isAreaName('....')).toBe(false)
35 | expect(isAreaName('.......')).toBe(false)
36 | })
37 | })
38 | })
39 | })
40 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/strings/isAreaName/isAreaName.ts:
--------------------------------------------------------------------------------
1 | export const keywords = [
2 | // Dot symbol (and its sequence) is a valid placeholder,
3 | // but not a valid CSS Grid area name.
4 | /\.+/,
5 | // Numbers may be present in `grid-template` definition
6 | // and describe dimensions of rows/columns.
7 | /^[0-9]/,
8 | // Slash is a special symbol used to declare dimensions
9 | // for columns.
10 | '/',
11 | // "auto" is a reserved keyword to describe an automatic
12 | // dimension value when sizing rows/columns.
13 | 'auto',
14 | ]
15 |
16 | /**
17 | * Determines if a given string is a valid CSS Grid area name.
18 | * Takes into account row/column dimensions and reserved
19 | * keywords used in the `grid-template` definition.
20 | */
21 | export default function isAreaName(areaName: string): boolean {
22 | return keywords.every((keyword) => {
23 | return keyword instanceof RegExp
24 | ? !keyword.test(areaName)
25 | : areaName !== keyword
26 | })
27 | }
28 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/strings/parsePropName/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './parsePropName'
2 | export * from './parsePropName'
3 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/strings/parsePropName/parsePropName.ts:
--------------------------------------------------------------------------------
1 | import { BreakpointBehavior } from '../../../const/defaultOptions'
2 | import Layout from '../../../Layout'
3 | import toLowerCaseFirst from '../toLowerCaseFirst'
4 | import capitalize from '../capitalize'
5 |
6 | export interface Props {
7 | [propName: string]: any
8 | }
9 |
10 | export interface ParsedProp {
11 | originPropName: string
12 | purePropName: string
13 | breakpoint: ParsedBreakpoint
14 | behavior: BreakpointBehavior
15 | }
16 |
17 | export interface ParsedBreakpoint {
18 | name: string
19 | isDefault: boolean
20 | }
21 |
22 | /**
23 | * Returns a parsed prop summary, which includes pure prop name,
24 | * an optional breakpoint name and breakpoint behavior.
25 | *
26 | * \w+(?<=(sm)?(only)?)$
27 | * This RegExp also works well. May consider implementing once
28 | * lookbehind is supported everywhere.
29 | */
30 | export default function parsePropName(originPropName: string): ParsedProp {
31 | const joinedBreakpointNames = Object.keys(Layout.breakpoints)
32 | .map(capitalize)
33 | .join('|')
34 | const joinedBehaviors = ['down', 'only'].map(capitalize).join('|')
35 | const breakpointExp = new RegExp(`(${joinedBreakpointNames})$`, 'g')
36 | const behaviorExp = new RegExp(`(${joinedBehaviors})$`, 'g')
37 |
38 | const behaviorMatch = originPropName.match(behaviorExp)
39 | const behavior = behaviorMatch ? behaviorMatch[0] : ''
40 | const breakpointMatch = originPropName
41 | .replace(behavior, '')
42 | .match(breakpointExp)
43 | const breakpointName = breakpointMatch ? breakpointMatch[0] : ''
44 | const purePropName = originPropName
45 | .replace(breakpointName, '')
46 | .replace(behavior, '')
47 |
48 | /**
49 | * Get normalized breakpoint name.
50 | * When a breakpoint name is a part of the prop name, covert it first letter
51 | * to lowercase to match the layout options. Otherwise, take the default
52 | * breakpoint name.
53 | */
54 | const normalizedBreakpointName = breakpointName
55 | ? toLowerCaseFirst(breakpointName)
56 | : Layout.defaultBreakpointName
57 |
58 | const isDefaultBreakpoint =
59 | normalizedBreakpointName === Layout.defaultBreakpointName
60 |
61 | return {
62 | originPropName,
63 | purePropName,
64 | behavior: behavior
65 | ? toLowerCaseFirst(behavior)
66 | : Layout.defaultBehavior,
67 | breakpoint: {
68 | name: normalizedBreakpointName,
69 | isDefault: isDefaultBreakpoint,
70 | },
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/strings/sanitizeTemplateArea/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './sanitizeTemplateArea'
2 | export * from './sanitizeTemplateArea'
3 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/strings/sanitizeTemplateArea/sanitizeTemplateArea.spec.ts:
--------------------------------------------------------------------------------
1 | import sanitizeTemplateArea from './sanitizeTemplateArea'
2 |
3 | describe('sanitizeTemplateArea', () => {
4 | describe('given an arbitrary template string', () => {
5 | it('should enforce single quotes for new lines', () => {
6 | expect(sanitizeTemplateArea('foo')).toEqual(`'foo'`)
7 | expect(sanitizeTemplateArea('foo bar')).toEqual(`'foo bar'`)
8 | })
9 | })
10 |
11 | describe('given a template string with duplicate quotes', () => {
12 | it('should return a parsed template string with deduplicated quotes', () => {
13 | expect(sanitizeTemplateArea("'foo'")).toEqual(`'foo'`)
14 | expect(sanitizeTemplateArea("'foo bar'")).toEqual(`'foo bar'`)
15 | })
16 | })
17 |
18 | describe('given "grid-template" syntax', () => {
19 | it('should parse single area definition', () => {
20 | expect(sanitizeTemplateArea('foo bar 100px')).toEqual(`'foo bar' 100px`)
21 | })
22 |
23 | it('should parse multi-area definition', () => {
24 | expect(sanitizeTemplateArea('first second 2fr')).toEqual(
25 | `'first second' 2fr`,
26 | )
27 | })
28 |
29 | it('should return "grid-template-columns" dimensions without quoutes', () => {
30 | expect(sanitizeTemplateArea('/ 200px auto 1fr')).toEqual(
31 | `/ 200px auto 1fr`,
32 | )
33 | })
34 | })
35 |
36 | describe('given a template string with placeholder dots', () => {
37 | it('should return a parsed template string including dots', () => {
38 | expect(sanitizeTemplateArea('first . second ... third')).toEqual(
39 | `'first . second ... third'`,
40 | )
41 | })
42 | })
43 | })
44 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/strings/sanitizeTemplateArea/sanitizeTemplateArea.ts:
--------------------------------------------------------------------------------
1 | import compose from '../../functions/compose'
2 | import isAreaName from '../isAreaName'
3 |
4 | type SanitizeTemplateArea = (str: string) => string
5 |
6 | /**
7 | * Joins a given template string fragments into a valid template string.
8 | * Appends any row/column dimensions after the enclosing single quote
9 | * character to have a valid `grid-template` syntax.
10 | */
11 | const joinTemplateFragments = (fragments: string[]): string => {
12 | const areas: string[] = []
13 | const suffixes: string[] = []
14 |
15 | fragments.forEach((areaName) => {
16 | if (isAreaName(areaName) || /\.+/.test(areaName)) {
17 | areas.push(areaName)
18 | } else {
19 | suffixes.push(areaName)
20 | }
21 | })
22 |
23 | // Wraps areas string in single quote per CSS spec
24 | const joinedAreas = areas.length > 0 ? `'${areas.join(' ')}'` : ''
25 | const joinedSuffixes = suffixes.join(' ')
26 |
27 | // Ensures row/column dimensions follow areas list after its been
28 | // wrapped in single quotes.
29 | return [joinedAreas, joinedSuffixes].filter(Boolean).join(' ')
30 | }
31 |
32 | /**
33 | * Sanitizes a given `grid-template-areas` string.
34 | * Trims whitespaces, deduplicates quotes and wraps each line
35 | * in single quotes to be CSS-compliant.
36 | */
37 | const sanitizeTemplateArea: SanitizeTemplateArea = compose(
38 | joinTemplateFragments,
39 | (area: string): string[] => area.split(' '),
40 | (area: string): string => area.replace(/'+/gm, ''),
41 | (area: string): string => area.trim(),
42 | )
43 |
44 | export default sanitizeTemplateArea
45 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/strings/sanitizeTemplateString/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './sanitizeTemplateString'
2 | export * from './sanitizeTemplateString'
3 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/strings/sanitizeTemplateString/sanitizeTemplateString.ts:
--------------------------------------------------------------------------------
1 | import compose from '../../functions/compose'
2 | import isAreaName from '../isAreaName'
3 |
4 | type SanitizeTemplateString = (str: string) => string[]
5 |
6 | /**
7 | * Returns an array of unique normalized grid area names
8 | * from the given template string. Any member of the returned list
9 | * is later evolved into a React component.
10 | */
11 | const sanitizeTemplateString: SanitizeTemplateString = compose(
12 | (list: string[]): string[] => list.sort(),
13 |
14 | // Deduplicate area strings
15 | (list: string[]): string[] => Array.from(new Set(list)),
16 |
17 | // Filter out "template" row/columns sizes
18 | (arr: string[]): string[] => arr.filter(isAreaName),
19 |
20 | // Filter out empty area strings
21 | (arr: string[]): string[] => arr.filter(Boolean),
22 |
23 | // Split into a list of areas
24 | (str: string): string[] => str.split(' '),
25 |
26 | // Deduplicate multiple spaces
27 | (str: string): string => str.replace(/\s+/g, ' '),
28 |
29 | // Replace new lines and single quotes with spaces
30 | (str: string): string => str.replace(/\r?\n|\'+/g, ' '),
31 | )
32 |
33 | export default sanitizeTemplateString
34 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/strings/toDashedString/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './toDashedString'
2 | export * from './toDashedString'
3 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/strings/toDashedString/toDashedString.spec.ts:
--------------------------------------------------------------------------------
1 | import toDashedString from './toDashedString'
2 |
3 | describe('toDashedString', () => {
4 | describe('given a cammelCase string', () => {
5 | it('should convert the string to kebab-case', () => {
6 | expect(toDashedString('loremIpsum')).toBe('lorem-ipsum')
7 | expect(toDashedString('loremIpsumDolor')).toBe('lorem-ipsum-dolor')
8 | })
9 | })
10 |
11 | describe('given a string without a capital letter', () => {
12 | it('should return the string as-is', () => {
13 | expect(toDashedString('lorem')).toBe('lorem')
14 | })
15 | })
16 | })
17 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/strings/toDashedString/toDashedString.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Converts given cammelCase string into kebab-case.
3 | * @example
4 | * toDashedString('fooBar')
5 | * @returns "foo-bar"
6 | */
7 | export default function toDashedString(str: string): string {
8 | return str.replace(/[A-Z]/g, (capitalLetter) => {
9 | return `-${capitalLetter}`.toLowerCase()
10 | })
11 | }
12 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/strings/toLowerCaseFirst/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './toLowerCaseFirst'
2 | export * from './toLowerCaseFirst'
3 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/strings/toLowerCaseFirst/toLowerCaseFirst.spec.ts:
--------------------------------------------------------------------------------
1 | import toLowerCaseFirst from './toLowerCaseFirst'
2 |
3 | describe('toLowerCaseFirst', () => {
4 | describe('given a capitalized string', () => {
5 | it('should convert the first letter to lowercase', () => {
6 | expect(toLowerCaseFirst('Foo')).toBe('foo')
7 | expect(toLowerCaseFirst('FooBar')).toBe('fooBar')
8 | })
9 | })
10 |
11 | describe('given a string with first latter already being lowercase', () => {
12 | it('should return the string as-is', () => {
13 | expect(toLowerCaseFirst('foo')).toBe('foo')
14 | expect(toLowerCaseFirst('fooBar')).toBe('fooBar')
15 | })
16 | })
17 | })
18 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/strings/toLowerCaseFirst/toLowerCaseFirst.ts:
--------------------------------------------------------------------------------
1 | export default function toLowerCaseFirst(str: string): R {
2 | return (str.slice(0, 1).toLowerCase() + str.slice(1, str.length)) as any
3 | }
4 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/styles/applyStyles/applyStyles.ts:
--------------------------------------------------------------------------------
1 | import Layout from '../../../Layout'
2 | import { BreakpointBehavior } from '../../../const/defaultOptions'
3 | import propAliases from '../../../const/propAliases'
4 | import parsePropName, {
5 | Props,
6 | ParsedBreakpoint,
7 | } from '../../strings/parsePropName'
8 | import isset from '../../functions/isset'
9 | import createMediaQuery from '../createMediaQuery'
10 |
11 | /**
12 | * Generateds a single CSS string for the set of props,
13 | * value, and its responsive information.
14 | */
15 | const createStyleString = (
16 | cssProps: string[],
17 | propValue: any,
18 | parsedBreakpoint: ParsedBreakpoint,
19 | behavior: BreakpointBehavior,
20 | ) => {
21 | const styleString = cssProps
22 | .map((propName) => `${propName}:${String(propValue)};`)
23 | .join('')
24 |
25 | // Get a breakpoint dimensions based on the statically analyzed breakpoint.
26 | const breakpointOptions = Layout.breakpoints[parsedBreakpoint.name]
27 |
28 | // Wrap CSS rule in a media query only if its prop includes
29 | // a breakpoint and behavior different from the default ones.
30 | const shouldWrapInMediaQuery =
31 | breakpointOptions &&
32 | !(parsedBreakpoint.isDefault && behavior === Layout.defaultBehavior)
33 |
34 | return shouldWrapInMediaQuery
35 | ? `@media ${createMediaQuery(breakpointOptions, behavior)} {${styleString}}`
36 | : styleString
37 | }
38 |
39 | /**
40 | * Transforms known prop aliases to CSS rules for the given props.
41 | */
42 | export default function applyStyles(pristineProps: Props): string {
43 | return (
44 | Object.keys(pristineProps)
45 | // Parse each prop to include "breakpoint" and "behavior"
46 | .map(parsePropName)
47 | // Filter out props that are not included in prop aliases
48 | .filter(({ purePropName }) => propAliases.hasOwnProperty(purePropName))
49 | // Filter out props with "undefined" or "null" as value
50 | .filter(({ originPropName }) => isset(pristineProps[originPropName]))
51 | // Map each prop to a CSS rule string
52 | .map(({ purePropName, originPropName, breakpoint, behavior }) => {
53 | const { props, transformValue } = propAliases[purePropName]
54 | const propValue = pristineProps[originPropName]
55 | const transformedPropValue = transformValue
56 | ? transformValue(propValue)
57 | : propValue
58 |
59 | return createStyleString(
60 | props,
61 | transformedPropValue,
62 | breakpoint,
63 | behavior,
64 | )
65 | })
66 | .join(' ')
67 | )
68 | }
69 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/styles/applyStyles/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './applyStyles'
2 | export * from './applyStyles'
3 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/styles/createMediaQuery/createMediaQuery.spec.ts:
--------------------------------------------------------------------------------
1 | import createMediaQuery from './createMediaQuery'
2 |
3 | describe('createMediaQuery', () => {
4 | describe('given a breakpoint with "up" behavior', () => {
5 | let mediaQuery: string
6 |
7 | beforeAll(() => {
8 | mediaQuery = createMediaQuery(
9 | {
10 | minWidth: 500,
11 | maxWidth: 765,
12 | },
13 | 'up',
14 | )
15 | })
16 |
17 | it('should not have any "max-" breakpoint properties', () => {
18 | expect(mediaQuery).not.toContain('maxWidth')
19 | })
20 |
21 | it('should return a media query string with "min-" breakpoint properties', () => {
22 | expect(mediaQuery).toEqual('(min-width:500px)')
23 | })
24 | })
25 |
26 | describe('given a breakpoint with "down" behavior', () => {
27 | let mediaQuery: string
28 |
29 | beforeAll(() => {
30 | mediaQuery = createMediaQuery(
31 | {
32 | minWidth: 400,
33 | maxWidth: 565,
34 | minResolution: '300dpi',
35 | },
36 | 'down',
37 | )
38 | })
39 |
40 | it('should not have any "min-" breakpoint properties', () => {
41 | expect(mediaQuery).not.toContain('minWidth')
42 | })
43 |
44 | it('should return a media query string with "max-" breakpoint properties', () => {
45 | expect(mediaQuery).toEqual(
46 | '(max-width:565px) and (min-resolution:300dpi)',
47 | )
48 | })
49 | })
50 |
51 | describe('given a breakpoint with "only" behavior', () => {
52 | let mediaQuery: string
53 |
54 | beforeAll(() => {
55 | mediaQuery = createMediaQuery(
56 | {
57 | minWidth: 768,
58 | maxWidth: 1120,
59 | orientation: 'landscape',
60 | },
61 | 'only',
62 | )
63 | })
64 |
65 | it('should return a media query string with all breakpoint properties', () => {
66 | expect(mediaQuery).toContain(
67 | '(min-width:768px) and (max-width:1120px) and (orientation:landscape)',
68 | )
69 | })
70 | })
71 | })
72 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/styles/createMediaQuery/createMediaQuery.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Numeric,
3 | Breakpoint,
4 | BreakpointBehavior,
5 | } from '../../../const/defaultOptions'
6 | import transformNumeric from '../../math/transformNumeric'
7 | import normalizeQuery from '../../styles/normalizeQuery'
8 | import compose from '../../functions/compose'
9 |
10 | type MediaQueryPair = [string, Numeric]
11 |
12 | /**
13 | * Determines whether a given media query param should be added
14 | * to the media query string based on a breakpoint's behavior.
15 | */
16 | const shouldAppendProperty = (
17 | queryParam: string,
18 | behavior: BreakpointBehavior,
19 | ): boolean => {
20 | const [prefix, splitPropName] = queryParam.split('-')
21 | const isDimensionalProp = ['height', 'width'].includes(splitPropName)
22 |
23 | if (!isDimensionalProp) {
24 | return true
25 | }
26 |
27 | return (
28 | (prefix === 'min' && ['up', 'only'].includes(behavior)) ||
29 | (prefix === 'max' && ['down', 'only'].includes(behavior))
30 | )
31 | }
32 |
33 | const filterRelevantQueryParams = (behavior: BreakpointBehavior) => (
34 | queryList: MediaQueryPair[],
35 | ): MediaQueryPair[] => {
36 | return queryList.filter(([queryParam]) =>
37 | shouldAppendProperty(queryParam, behavior),
38 | )
39 | }
40 |
41 | /**
42 | * Joins a given media query params list with the given transformer function.
43 | */
44 | export const joinQueryList = (transformer: (pair: MediaQueryPair) => any) => (
45 | queryList: MediaQueryPair[],
46 | ) => {
47 | return queryList.map(transformer).join(' and ')
48 | }
49 |
50 | export default function createMediaQuery(
51 | breakpoint: Breakpoint,
52 | behavior: BreakpointBehavior,
53 | ): string {
54 | return compose(
55 | joinQueryList(([dashedQueryProp, propValue]) => {
56 | return `(${dashedQueryProp}:${String(transformNumeric(propValue))})`
57 | }),
58 | filterRelevantQueryParams(behavior),
59 | normalizeQuery,
60 | )(breakpoint)
61 | }
62 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/styles/createMediaQuery/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './createMediaQuery'
2 | export * from './createMediaQuery'
3 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/styles/normalizeQuery/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './normalizeQuery'
2 | export * from './normalizeQuery'
3 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/styles/normalizeQuery/normalizeQuery.spec.ts:
--------------------------------------------------------------------------------
1 | import normalizeQuery from './normalizeQuery'
2 |
3 | describe('normalizeQuery', () => {
4 | describe('given a media query Object', () => {
5 | it('returns its [key, value] pairs', () => {
6 | expect(
7 | normalizeQuery({
8 | minWidth: 120,
9 | maxAspectRatio: '3/4',
10 | }),
11 | ).toEqual([['min-width', 120], ['max-aspect-ratio', '3/4']])
12 | })
13 | })
14 | })
15 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/styles/normalizeQuery/normalizeQuery.ts:
--------------------------------------------------------------------------------
1 | import { Numeric, Breakpoint } from '../../../const/defaultOptions'
2 | import isset from '../../functions/isset'
3 | import toDashedString from '../../strings/toDashedString'
4 |
5 | /**
6 | * Normalizes given media query object to a list of [propName, propValue].
7 | * @example
8 | * normalizeQuery({ minWidth: 120 })
9 | * // [['min-width', 120]]
10 | */
11 | export default function normalizeQuery(
12 | queryProps: Breakpoint,
13 | ): Array<[string, Numeric]> {
14 | return Object.entries(queryProps)
15 | .filter(([_, propValue]) => isset(propValue))
16 | .map<[string, Numeric]>(([propName, propValue]) => [
17 | toDashedString(propName),
18 | propValue,
19 | ])
20 | }
21 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/templates/generateComponents/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './generateComponents'
2 | export * from './generateComponents'
3 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/templates/getAreasList/getAreasList.spec.ts:
--------------------------------------------------------------------------------
1 | import Layout from '../../../Layout'
2 | import filterTemplateProps from '../parseTemplates/filterTemplateProps'
3 | import getAreasList from './getAreasList'
4 |
5 | const withProps = filterTemplateProps
6 |
7 | describe('getAreasList', () => {
8 | it('parses template props properly', () => {
9 | const areasList = getAreasList(
10 | withProps({
11 | template: `a b`,
12 | }),
13 | )
14 |
15 | expect(areasList).toEqual({
16 | areas: ['a', 'b'],
17 | templates: [
18 | {
19 | areas: ['a', 'b'],
20 | behavior: 'up',
21 | breakpoint: Layout.breakpoints.xs,
22 | },
23 | ],
24 | })
25 | })
26 |
27 | it('returns proper areas list for "up" behavior', () => {
28 | const areasList = getAreasList(
29 | withProps({
30 | template: `a b`,
31 | templateMd: `a b c`,
32 | }),
33 | )
34 |
35 | expect(areasList).toEqual({
36 | areas: ['a', 'b', 'c'],
37 | templates: [
38 | {
39 | areas: ['a', 'b'],
40 | behavior: 'up',
41 | breakpoint: Layout.breakpoints.xs,
42 | },
43 | {
44 | areas: ['a', 'b', 'c'],
45 | behavior: 'up',
46 | breakpoint: Layout.breakpoints.md,
47 | },
48 | ],
49 | })
50 | })
51 |
52 | it('returns proper areas list for "down" behavior', () => {
53 | const areasList = getAreasList(
54 | withProps({
55 | template: `a b`,
56 | templateMdDown: `c`,
57 | }),
58 | )
59 |
60 | expect(areasList).toEqual({
61 | areas: ['a', 'b', 'c'],
62 | templates: [
63 | {
64 | areas: ['a', 'b'],
65 | behavior: 'up',
66 | breakpoint: Layout.breakpoints.xs,
67 | },
68 | {
69 | areas: ['c'],
70 | behavior: 'down',
71 | breakpoint: Layout.breakpoints.md,
72 | },
73 | ],
74 | })
75 | })
76 |
77 | it('returns proper areas list for "only" behavior', () => {
78 | const areasList = getAreasList(
79 | withProps({
80 | template: `a`,
81 | templateMdOnly: `b c`,
82 | }),
83 | )
84 |
85 | expect(areasList).toEqual({
86 | areas: ['a', 'b', 'c'],
87 | templates: [
88 | {
89 | areas: ['a'],
90 | behavior: 'up',
91 | breakpoint: Layout.breakpoints.xs,
92 | },
93 | {
94 | areas: ['b', 'c'],
95 | behavior: 'only',
96 | breakpoint: Layout.breakpoints.md,
97 | },
98 | ],
99 | })
100 | })
101 | })
102 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/templates/getAreasList/getAreasList.ts:
--------------------------------------------------------------------------------
1 | import { Breakpoint, BreakpointBehavior } from '../../../const/defaultOptions'
2 | import Layout from '../../../Layout'
3 | import parsePropName from '../../strings/parsePropName'
4 |
5 | export interface Template {
6 | breakpoint: Breakpoint
7 | behavior: BreakpointBehavior
8 | areas: string[]
9 | }
10 |
11 | export interface AreasList {
12 | areas: string[]
13 | templates: Template[]
14 | }
15 |
16 | export interface TemplateProps {
17 | [propName: string]: string[]
18 | }
19 |
20 | export default function getAreasList(templateProps: TemplateProps): AreasList {
21 | const areasList = Object.entries(templateProps).reduce(
22 | (acc, [templateName, templateAreas]) => {
23 | const { breakpoint, behavior } = parsePropName(templateName)
24 | const nextAreas = acc.areas.concat(templateAreas)
25 | const nextTemplates = acc.templates.concat({
26 | breakpoint: Layout.breakpoints[breakpoint.name],
27 | behavior,
28 | areas: templateAreas,
29 | })
30 |
31 | return {
32 | areas: nextAreas,
33 | templates: nextTemplates,
34 | }
35 | },
36 | {
37 | areas: [],
38 | templates: [],
39 | },
40 | )
41 |
42 | const { areas, templates } = areasList
43 |
44 | return {
45 | areas: Array.from(new Set(areas)),
46 | templates,
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/templates/getAreasList/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './getAreasList'
2 | export * from './getAreasList'
3 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/templates/parseTemplates/filterTemplateProps.spec.ts:
--------------------------------------------------------------------------------
1 | import filterTemplateProps, { TemplateProps } from './filterTemplateProps'
2 |
3 | describe('filterTemplateProps', () => {
4 | describe('given an object with arbitrary props', () => {
5 | let templateProps: TemplateProps
6 |
7 | beforeAll(() => {
8 | templateProps = filterTemplateProps({
9 | template: 'first',
10 | templateOnly: 'three',
11 | templateMd: 'second',
12 | templateCols: true,
13 | templateBars: true,
14 |
15 | randomProp: 'yes',
16 | yetAnotherUknownProp: true,
17 | })
18 | })
19 |
20 | it('should ignore non-template props', () => {
21 | expect(templateProps).not.toContain('randomProp')
22 | expect(templateProps).not.toContain('yetAnotherUknownProp')
23 | })
24 |
25 | it('should return template props', () => {
26 | expect(templateProps).toEqual({
27 | template: ['first'],
28 | templateOnly: ['three'],
29 | templateMd: ['second'],
30 | })
31 | })
32 | })
33 | })
34 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/templates/parseTemplates/filterTemplateProps.ts:
--------------------------------------------------------------------------------
1 | import parsePropName, { Props } from '../../strings/parsePropName'
2 | import sanitizeTemplateString from '../../strings/sanitizeTemplateString'
3 |
4 | export interface TemplateProps {
5 | [templateProp: string]: string[]
6 | }
7 |
8 | /**
9 | * Accepts a props object and filters it to include
10 | * only template-related prop:value pairs.
11 | */
12 | const filterTemplateProps = (props: Props): TemplateProps => {
13 | return Object.keys(props)
14 | .filter((propName) => {
15 | const { purePropName } = parsePropName(propName)
16 | return ['areas', 'template'].includes(purePropName)
17 | })
18 | .reduce((acc, propName) => {
19 | return {
20 | ...acc,
21 | [propName]: sanitizeTemplateString(props[propName]),
22 | }
23 | }, {})
24 | }
25 |
26 | export default filterTemplateProps
27 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/templates/parseTemplates/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './parseTemplates'
2 | export * from './parseTemplates'
3 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/src/utils/templates/parseTemplates/parseTemplates.ts:
--------------------------------------------------------------------------------
1 | import filterTemplateProps, { TemplateProps } from './filterTemplateProps'
2 | import getAreasList, { AreasList } from '../getAreasList'
3 | import compose from '../../functions/compose'
4 | import memoizeWith from '../../functions/memoizeWith'
5 | import hashString from '../../strings/hashString'
6 | import { Props } from '../../strings/parsePropName'
7 |
8 | type ParseTemplates = (props: Props) => AreasList
9 |
10 | /**
11 | * Memoize areas generation based on the sanitized "templateProp:areas" pairs.
12 | * Alphabetical sorting of incoming template areas allows reproducible cache keys.
13 | * @todo `pairs` is an empty array sometimes. Should we handle it somehow?
14 | */
15 | const memoizeProps = memoizeWith((templateProps: TemplateProps) => {
16 | const pairs = Object.entries(templateProps).reduce(
17 | (acc, [propName, templateAreas]) => {
18 | return acc.concat(`${propName}:${templateAreas.join()}`)
19 | },
20 | [],
21 | )
22 |
23 | return hashString(pairs.join()).toString()
24 | })
25 |
26 | /**
27 | * Parses a given map of props and returns an areas map.
28 | */
29 | const parseTemplates: ParseTemplates = compose(
30 | memoizeProps(getAreasList),
31 | filterTemplateProps,
32 | )
33 |
34 | export default parseTemplates
35 |
--------------------------------------------------------------------------------
/packages/atomic-layout-core/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": true,
3 | "compilerOptions": {
4 | "module": "es6",
5 | "target": "esnext",
6 | "moduleResolution": "node",
7 | "declaration": true,
8 | "noImplicitThis": false,
9 | "noImplicitAny": true,
10 | "esModuleInterop": true,
11 | "baseUrl": ".",
12 | "types": ["node", "jest"]
13 | },
14 | "include": ["global.d.ts", "src/**/*"],
15 | "exclude": ["node_modules", "**/*.spec.ts"]
16 | }
17 |
--------------------------------------------------------------------------------
/packages/atomic-layout-emotion/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Artem Zakharchenko
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/packages/atomic-layout-emotion/babel.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 |
3 | const babelConfigPath = path.resolve(__dirname, '../..', 'babel.config.js')
4 |
5 | console.log(
6 | '@atomic-layout/emotion: loading babel config at "%s"',
7 | babelConfigPath,
8 | )
9 |
10 | module.exports = {
11 | // Resolve the path otherwise it gets resolved relatively
12 | // to Storybook main entry module during the Storybook build.
13 | extends: babelConfigPath,
14 | plugins: [
15 | [
16 | 'transform-rename-import',
17 | {
18 | // Replace imports to "styled-components" with imports to emotion.
19 | // Applied to the modules imported from "atomic-layout" package.
20 | original: '^styled-components',
21 | replacement: '@emotion/styled',
22 | },
23 | ],
24 | ],
25 | }
26 |
--------------------------------------------------------------------------------
/packages/atomic-layout-emotion/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | name: 'Atomic Layout (emotion)',
3 | verbose: true,
4 | roots: ['src', 'test'],
5 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
6 | testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$',
7 | transform: {
8 | '^.+\\.tsx?$': 'ts-jest',
9 | '^.+\\.(js|jsx)$': 'babel-jest',
10 | },
11 | }
12 |
--------------------------------------------------------------------------------
/packages/atomic-layout-emotion/logo-full.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kettanaito/atomic-layout/3df0c1c33739f627db72d03a005b4ee9c7b8aad6/packages/atomic-layout-emotion/logo-full.png
--------------------------------------------------------------------------------
/packages/atomic-layout-emotion/src/index.ts:
--------------------------------------------------------------------------------
1 | import { Layout } from '@atomic-layout/core'
2 | export default Layout
3 |
4 | export {
5 | /* Components */
6 | Box,
7 | Composition,
8 | Only,
9 | Visible,
10 | /* Hooks */
11 | useMediaQuery,
12 | useViewportChange,
13 | useBreakpointChange,
14 | useResponsiveValue,
15 | useResponsiveProps,
16 | useResponsiveQuery,
17 | /* Utils */
18 | query,
19 | makeResponsive,
20 | } from '../../atomic-layout/src'
21 |
--------------------------------------------------------------------------------
/packages/atomic-layout-emotion/test/refs.spec.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Composition, Only, Visible } from '../src'
2 | import { createForwardRefTest } from '../../atomic-layout/test/createForwardRefTest'
3 |
4 | createForwardRefTest({ Box, Composition, Only, Visible })
5 |
--------------------------------------------------------------------------------
/packages/atomic-layout-emotion/test/ssr.spec.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * @jest-environment node
3 | */
4 | import { createSsrTest } from '../../atomic-layout/test/createSsrTest'
5 |
6 | createSsrTest(() => import('../src/index'))
7 |
--------------------------------------------------------------------------------
/packages/atomic-layout-emotion/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": true,
3 | "compilerOptions": {
4 | "module": "es6",
5 | "target": "esnext",
6 | "moduleResolution": "node",
7 | "declaration": true,
8 | "emitDeclarationOnly": true,
9 | "declarationDir": "lib/types",
10 | "noImplicitThis": false,
11 | "noImplicitAny": true,
12 | "esModuleInterop": true,
13 | "jsx": "react",
14 | "baseUrl": ".",
15 | "types": ["jest"],
16 | "rootDirs": ["src", "../atomic-layout/src"]
17 | },
18 | "include": ["src/**/*"],
19 | "exclude": ["node_modules", "**/*.spec.ts"]
20 | }
21 |
--------------------------------------------------------------------------------
/packages/atomic-layout-emotion/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["../atomic-layout/tslint.json"]
3 | }
4 |
--------------------------------------------------------------------------------
/packages/atomic-layout/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Artem Zakharchenko
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/packages/atomic-layout/babel.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 |
3 | const babelConfigPath = path.resolve(__dirname, '../..', 'babel.config.js')
4 |
5 | console.log(
6 | '@atomic-layout/styled: loading babel config at "%s"',
7 | babelConfigPath,
8 | )
9 |
10 | module.exports = {
11 | // Resolve the path otherwise it gets resolved relatively
12 | // to Storybook main entry module during the Storybook build.
13 | extends: babelConfigPath,
14 | }
15 |
--------------------------------------------------------------------------------
/packages/atomic-layout/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | name: 'Atomic Layout (styled-components)',
3 | verbose: true,
4 | roots: ['src', 'test'],
5 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
6 | testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$',
7 | transform: {
8 | '^.+\\.tsx?$': 'ts-jest',
9 | '^.+\\.(js|jsx)$': 'babel-jest',
10 | },
11 | }
12 |
--------------------------------------------------------------------------------
/packages/atomic-layout/logo-full.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kettanaito/atomic-layout/3df0c1c33739f627db72d03a005b4ee9c7b8aad6/packages/atomic-layout/logo-full.png
--------------------------------------------------------------------------------
/packages/atomic-layout/src/components/Box.spec.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { render, cleanup, getByText } from '@testing-library/react'
3 | import '@testing-library/jest-dom/extend-expect'
4 | import Box from './Box'
5 |
6 | describe('Box', () => {
7 | afterEach(cleanup)
8 |
9 | it('renders default Box', () => {
10 | const { container } = render(Content )
11 | const renderedBox = getByText(container, 'Content')
12 |
13 | expect(renderedBox).toHaveTextContent('Content')
14 | expect(renderedBox).toHaveStyle('display:block')
15 | expect(renderedBox).toHaveStyle('padding:10px')
16 | })
17 |
18 | it('renders inline Box', () => {
19 | const { container } = render(
20 |
21 | Content
22 | ,
23 | )
24 | const renderedBox = getByText(container, 'Content')
25 |
26 | expect(renderedBox).toHaveTextContent('Content')
27 | expect(renderedBox).toHaveStyle('display:inline-block')
28 | })
29 |
30 | it('supports flexbox display model', () => {
31 | const { container } = render(
32 |
33 | Content
34 | ,
35 | )
36 | const renderedBlock = getByText(container, 'Content')
37 |
38 | expect(renderedBlock).toHaveTextContent('Content')
39 | expect(renderedBlock).toHaveStyle('display:flex')
40 | expect(renderedBlock).toHaveStyle('align-items:center')
41 | })
42 |
43 | it('supports inline flexbox display model', () => {
44 | const { container } = render(
45 |
46 | Content
47 | ,
48 | )
49 | const renderedBlock = getByText(container, 'Content')
50 |
51 | expect(renderedBlock).toHaveTextContent('Content')
52 | expect(renderedBlock).toHaveStyle('display:inline-flex')
53 | expect(renderedBlock).toHaveStyle('justify-content:center')
54 | })
55 | })
56 |
--------------------------------------------------------------------------------
/packages/atomic-layout/src/components/Box.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import styled from 'styled-components'
3 | import { BoxProps, applyStyles } from '@atomic-layout/core'
4 |
5 | const Box: React.FC = styled.div`
6 | display: ${({ flex, inline }) =>
7 | flex
8 | ? inline
9 | ? 'inline-flex'
10 | : 'flex'
11 | : inline
12 | ? 'inline-block'
13 | : 'block'};
14 |
15 | && {
16 | ${applyStyles};
17 | }
18 | `
19 |
20 | Box.displayName = 'Box'
21 |
22 | export default Box
23 |
--------------------------------------------------------------------------------
/packages/atomic-layout/src/components/Composition.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import styled from 'styled-components'
3 | import {
4 | BoxProps,
5 | CompositionProps,
6 | CompositionRenderProp,
7 | AreaComponent,
8 | parseTemplates,
9 | generateComponents,
10 | applyStyles,
11 | warn,
12 | } from '@atomic-layout/core'
13 | import Box from './Box'
14 | import { withPlaceholder } from '../utils/withPlaceholder'
15 | import { forwardRef } from '../utils/forwardRef'
16 |
17 | const CompositionWrapper = styled.div`
18 | && {
19 | ${applyStyles};
20 | display: ${({ inline }) => (inline ? 'inline-grid' : 'grid')};
21 | }
22 | `
23 |
24 | const createAreaComponent = (areaName: string): AreaComponent =>
25 | forwardRef((props: BoxProps, ref) => {
26 | return
27 | })
28 |
29 | const Composition = forwardRef(
30 | ({ children, ...restProps }, ref) => {
31 | const areasList = parseTemplates(restProps)
32 |
33 | // Memoize areas generation so parental updates do not re-generate areas,
34 | // making area components preserve their internal state.
35 | const Areas = React.useMemo(() => {
36 | return generateComponents(areasList, createAreaComponent, withPlaceholder)
37 | }, [areasList])
38 |
39 | const hasAreaComponents = Object.keys(Areas).length > 0
40 | const childrenType = typeof children
41 | const hasChildrenFunction = childrenType === 'function'
42 |
43 | // Warn when provided "areas"/"template" props, but didn't use a render prop pattern.
44 | warn(
45 | !(hasAreaComponents && !hasChildrenFunction),
46 | `Failed to render 'Composition' with template areas ["${Object.keys(
47 | Areas,
48 | ).join(
49 | '", "',
50 | )}"]: expected children to be a function, but got: ${childrenType}. Please provide render function as children, or remove assigned template props (\`areas\`/\`template\`).`,
51 | )
52 |
53 | return (
54 |
55 | {hasAreaComponents && hasChildrenFunction
56 | ? (children as CompositionRenderProp)(Areas)
57 | : children}
58 |
59 | )
60 | },
61 | )
62 |
63 | Composition.displayName = 'Composition'
64 |
65 | export default Composition
66 |
--------------------------------------------------------------------------------
/packages/atomic-layout/src/components/Only.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { BoxProps } from '@atomic-layout/core'
3 | import Box from './Box'
4 | import useResponsiveQuery, {
5 | ResponsiveQueryParams,
6 | } from '../hooks/useResponsiveQuery'
7 | import { forwardRef } from '../utils/forwardRef'
8 |
9 | export type OnlyProps = BoxProps & ResponsiveQueryParams
10 |
11 | const Only = forwardRef(
12 | ({ children, except, for: exactBreakpoint, from, to, ...restProps }, ref) => {
13 | const matches = useResponsiveQuery({
14 | for: exactBreakpoint,
15 | from,
16 | to,
17 | except,
18 | })
19 |
20 | return (
21 | matches && (
22 |
23 | {children}
24 |
25 | )
26 | )
27 | },
28 | )
29 |
30 | Only.displayName = 'Only'
31 |
32 | export default Only
33 |
--------------------------------------------------------------------------------
/packages/atomic-layout/src/components/Visible.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 | import Box from './Box'
4 | import { OnlyProps } from './Only'
5 | import useResponsiveQuery from '../hooks/useResponsiveQuery'
6 | import { forwardRef } from '../utils/forwardRef'
7 |
8 | const VisibleContainer = styled(Box)<{ matches: boolean }>`
9 | ${({ matches }) =>
10 | !matches &&
11 | `
12 | visibility: hidden;
13 | `}
14 | `
15 |
16 | const Visible = forwardRef(
17 | (
18 | { children, except, for: exactBreakpointName, from, to, ...boxProps },
19 | ref,
20 | ) => {
21 | const matches = useResponsiveQuery({
22 | except,
23 | for: exactBreakpointName,
24 | from,
25 | to,
26 | })
27 | const ariaAttributes = !matches ? { 'aria-hidden': 'true' } : {}
28 |
29 | return (
30 |
36 | {children}
37 |
38 | )
39 | },
40 | )
41 |
42 | Visible.displayName = 'Visible'
43 |
44 | export default Visible
45 |
--------------------------------------------------------------------------------
/packages/atomic-layout/src/hooks/useBreakpointChange.ts:
--------------------------------------------------------------------------------
1 | import { Layout, Breakpoints, createMediaQuery } from '@atomic-layout/core'
2 | import useViewportChange from './useViewportChange'
3 |
4 | /**
5 | * Executes a given callback upon any breakpoint change.
6 | * Callback calls are throttled by default.
7 | */
8 | const useBreakpointChange = (
9 | callback: (breakpointName: string) => void,
10 | throttleInterval?: number,
11 | breakpoints: Breakpoints = Layout.breakpoints,
12 | ) => {
13 | let prevBreakpointName: string
14 |
15 | useViewportChange(() => {
16 | const nextBreakpointName = Object.keys(breakpoints).find(
17 | (breakpointName) => {
18 | const mediaQuery = createMediaQuery(breakpoints[breakpointName], 'only')
19 | return matchMedia(mediaQuery).matches
20 | },
21 | )
22 |
23 | // Executes the callback only when breakpoint name has changed
24 | // between viewport changes.
25 | if (prevBreakpointName !== nextBreakpointName) {
26 | callback(nextBreakpointName)
27 | prevBreakpointName = nextBreakpointName
28 | }
29 | }, throttleInterval)
30 | }
31 |
32 | export default useBreakpointChange
33 |
--------------------------------------------------------------------------------
/packages/atomic-layout/src/hooks/useMediaQuery.ts:
--------------------------------------------------------------------------------
1 | import { useState, useMemo, useEffect, useLayoutEffect } from 'react'
2 | import {
3 | MediaQuery as MediaQueryParams,
4 | compose,
5 | joinQueryList,
6 | normalizeQuery,
7 | transformNumeric,
8 | } from '@atomic-layout/core'
9 |
10 | /**
11 | * Creates a media querty string based on the given params.
12 | */
13 | export const createMediaQuery = (queryParams: MediaQueryParams): string => {
14 | return compose(
15 | joinQueryList(([paramName, paramValue]) => {
16 | /**
17 | * Transform values that begin with a number to prevent
18 | * transformations of "calc" expressions.
19 | * Transformation of numerics is necessary when a simple
20 | * number is used as a value (min-width: 750) is not valid.
21 | *
22 | * (min-width: 750) ==> (min-width: 750px)
23 | */
24 | const resolvedParamValue = /^\d/.test(String(paramValue))
25 | ? transformNumeric(paramValue)
26 | : paramValue
27 |
28 | return `(${paramName}:${resolvedParamValue})`
29 | }),
30 | normalizeQuery,
31 | )(queryParams)
32 | }
33 |
34 | type UseMediaQuery = (
35 | queryParams: MediaQueryParams[] | MediaQueryParams,
36 | initialMatches?: boolean,
37 | ) => boolean
38 |
39 | export const useMediaQuery: UseMediaQuery = (
40 | queryParams,
41 | initialMatches = false,
42 | ): boolean => {
43 | const useSafeEffect =
44 | typeof window === 'undefined' ? useEffect : useLayoutEffect
45 | const [matches, setMatches] = useState(initialMatches)
46 | const query = useMemo(() => {
47 | return []
48 | .concat(queryParams)
49 | .map(createMediaQuery)
50 | .join(',')
51 | }, [queryParams])
52 |
53 | const handleMediaQueryChange = (
54 | mediaQueryList: MediaQueryList | MediaQueryListEvent,
55 | ) => {
56 | setMatches(mediaQueryList.matches)
57 | }
58 |
59 | useSafeEffect(() => {
60 | const mediaQueryList = matchMedia(query)
61 | handleMediaQueryChange(mediaQueryList)
62 | mediaQueryList.addListener(handleMediaQueryChange)
63 |
64 | return () => {
65 | mediaQueryList.removeListener(handleMediaQueryChange)
66 | }
67 | }, Object.keys(queryParams))
68 |
69 | return matches
70 | }
71 |
--------------------------------------------------------------------------------
/packages/atomic-layout/src/hooks/useResponsiveQuery.ts:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 | import { Breakpoint } from '@atomic-layout/core'
3 | import useViewportChange from './useViewportChange'
4 | import { query } from '../utils/query'
5 |
6 | export type BreakpointRef = Breakpoint | string
7 |
8 | export interface ResponsiveQueryParams {
9 | /**
10 | * Renders children only at the specified breakpoint.
11 | */
12 | for?: BreakpointRef
13 | /**
14 | * Renders children from the specified breakpoint and up,
15 | * unless enclosing `to` prop is set to form a range.
16 | */
17 | from?: BreakpointRef
18 | /**
19 | * Renders children from the specified breakpoint and down,
20 | * unless the openning `from` prop is set to form a range.
21 | */
22 | to?: BreakpointRef
23 | /**
24 | * Renders children everywhere except the given breakpoint range.
25 | */
26 | except?: boolean
27 | }
28 |
29 | /**
30 | * Returns a boolean indicating that the current viewport matches the given responsive query.
31 | * @example
32 | * const matches = useResponsiveQuery({ from: 'sm', to: 'lg' })
33 | * const matches = useResponsiveQuery({ except: true, from: 'md', to: 'lg' })
34 | */
35 | export default function useResponsiveQuery(
36 | params: ResponsiveQueryParams,
37 | initialMatches: boolean = false,
38 | ): boolean {
39 | const [matches, setMatches] = useState(initialMatches)
40 | const mediaQuery = query(params)
41 |
42 | useViewportChange(() => {
43 | const { matches: hasMatchingQuery } = matchMedia(mediaQuery)
44 | setMatches(hasMatchingQuery)
45 | })
46 |
47 | return matches
48 | }
49 |
--------------------------------------------------------------------------------
/packages/atomic-layout/src/hooks/useResponsiveValue.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react'
2 | import { withBreakpoints } from '@atomic-layout/core'
3 | import useBreakpointChange from './useBreakpointChange'
4 |
5 | /**
6 | * Accepts a map of { breakpointName: value } pairs
7 | * and returns a value based on the current viewport.
8 | * Returns default value when no matching pair is found.
9 | */
10 | const useResponsiveValue = (
11 | breakpoints: Record,
12 | defaultValue?: ValueType,
13 | ): ValueType => {
14 | const [value, setValue] = useState(defaultValue)
15 |
16 | const callback = () => {
17 | const nextValue = withBreakpoints(breakpoints, defaultValue)
18 | setValue(nextValue)
19 | }
20 |
21 | useEffect(callback, [breakpoints, defaultValue])
22 |
23 | useBreakpointChange(callback)
24 |
25 | return value
26 | }
27 |
28 | export default useResponsiveValue
29 |
--------------------------------------------------------------------------------
/packages/atomic-layout/src/hooks/useViewportChange.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from 'react'
2 | import { throttle } from '@atomic-layout/core'
3 |
4 | /**
5 | * Executes a callback on viewport change (window resize).
6 | * Callback calls are throttled by default.
7 | */
8 | const useViewportChange = (
9 | callback: () => void,
10 | throttleInterval: number = 70,
11 | ) => {
12 | const handleWindowResize = useRef<(...args: any[]) => any>()
13 |
14 | useEffect(() => {
15 | handleWindowResize.current = throttle(callback, throttleInterval)
16 | })
17 |
18 | useEffect(() => {
19 | const { current } = handleWindowResize
20 |
21 | current()
22 | window.addEventListener('resize', current)
23 | return () => window.removeEventListener('resize', current)
24 | }, [])
25 | }
26 |
27 | export default useViewportChange
28 |
--------------------------------------------------------------------------------
/packages/atomic-layout/src/index.ts:
--------------------------------------------------------------------------------
1 | import { Layout } from '@atomic-layout/core'
2 | export default Layout
3 |
4 | /* Components */
5 | export { default as Box } from './components/Box'
6 | export { default as Composition } from './components/Composition'
7 | export { default as Only } from './components/Only'
8 | export { default as Visible } from './components/Visible'
9 |
10 | /* Hooks */
11 | export { useMediaQuery } from './hooks/useMediaQuery'
12 | export { default as useViewportChange } from './hooks/useViewportChange'
13 | export { default as useBreakpointChange } from './hooks/useBreakpointChange'
14 | export { default as useResponsiveValue } from './hooks/useResponsiveValue'
15 | export { default as useResponsiveProps } from './hooks/useResponsiveProps'
16 | export { default as useResponsiveQuery } from './hooks/useResponsiveQuery'
17 |
18 | /* Utils */
19 | export { query } from './utils/query'
20 | export { makeResponsive } from './utils/makeResponsive'
21 |
--------------------------------------------------------------------------------
/packages/atomic-layout/src/utils/MakeResponsive.spec.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * @jest-environment node
3 | */
4 | import React from 'react'
5 | import { renderToString } from 'react-dom/server'
6 | import { makeResponsive } from './makeResponsive'
7 |
8 | const Component = makeResponsive((props) => {
9 | return
10 | })
11 |
12 | describe('makeResponsive', () => {
13 | describe('given rendered on a server', () => {
14 | let html: ReturnType
15 |
16 | beforeAll(() => {
17 | html = renderToString(
18 | ,
19 | )
20 | })
21 |
22 | it('should have responsive prop with default breakpoint', () => {
23 | expect(html).toContain('src="image.png"')
24 | })
25 |
26 | it.skip('should have responsive prop with "down" behavior', () => {
27 | expect(html).toContain('title="Title"')
28 | })
29 |
30 | it('should not have any responsive prop with other breakpoints', () => {
31 | expect(html).not.toContain('alt')
32 | })
33 | })
34 | })
35 |
--------------------------------------------------------------------------------
/packages/atomic-layout/src/utils/forwardRef.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | export const forwardRef = (
4 | component: React.RefForwardingComponent,
5 | ): React.FC => {
6 | return React.forwardRef(component)
7 | }
8 |
--------------------------------------------------------------------------------
/packages/atomic-layout/src/utils/getBreakpointsByQuery.test.ts:
--------------------------------------------------------------------------------
1 | import { getBreakpointsByQuery } from './getBreakpointsByQuery'
2 |
3 | describe('getBreakpointsByQuery', () => {
4 | describe('given an exact breakpoint (for)', () => {
5 | let result: ReturnType
6 |
7 | beforeAll(() => {
8 | result = getBreakpointsByQuery({ for: 'md' })
9 | })
10 |
11 | it('should return a single enclosed breakpoint', () => {
12 | expect(result).toEqual([
13 | {
14 | minWidth: '768px',
15 | maxWidth: '991px',
16 | },
17 | ])
18 | })
19 | })
20 |
21 | describe('given a high-pass breakpoint range (from)', () => {
22 | let result: ReturnType
23 |
24 | beforeAll(() => {
25 | result = getBreakpointsByQuery({ from: 'sm' })
26 | })
27 |
28 | it('should return breakpoints for that high-pass range', () => {
29 | expect(result).toEqual([
30 | {
31 | minWidth: '576px',
32 | },
33 | ])
34 | })
35 | })
36 |
37 | describe('given a low-pass breakpoint range (to)', () => {
38 | let result: ReturnType
39 |
40 | beforeAll(() => {
41 | result = getBreakpointsByQuery({ to: 'md' })
42 | })
43 |
44 | it('should return breakpoints for that low-pass range', () => {
45 | expect(result).toEqual([
46 | {
47 | maxWidth: '767px',
48 | },
49 | ])
50 | })
51 | })
52 |
53 | describe.only('given a bell breakpoint range (from/to)', () => {
54 | let result: ReturnType
55 |
56 | beforeAll(() => {
57 | result = getBreakpointsByQuery({ from: 'sm', to: 'lg' })
58 | })
59 |
60 | it('should return breakpoints for that inclusive range', () => {
61 | expect(result).toEqual([
62 | {
63 | minWidth: '576px',
64 | maxWidth: 'calc(992px - 1px)',
65 | },
66 | ])
67 | })
68 | })
69 |
70 | describe('given a notch breakpoint range (except/from/to)', () => {
71 | let result: ReturnType
72 |
73 | beforeAll(() => {
74 | result = getBreakpointsByQuery({ except: true, from: 'sm', to: 'lg' })
75 | })
76 |
77 | it('should return breakpoints for that exclusive range', () => {
78 | expect(result).toEqual([
79 | {
80 | maxWidth: '575px',
81 | },
82 | {
83 | maxWidth: undefined,
84 | minWidth: '992px',
85 | },
86 | ])
87 | })
88 | })
89 | })
90 |
--------------------------------------------------------------------------------
/packages/atomic-layout/src/utils/getBreakpointsByQuery.ts:
--------------------------------------------------------------------------------
1 | import {
2 | mergeAreaRecords,
3 | Breakpoint,
4 | Layout,
5 | openBreakpoint,
6 | closeBreakpoint,
7 | } from '@atomic-layout/core'
8 | import {
9 | ResponsiveQueryParams,
10 | BreakpointRef,
11 | } from '../hooks/useResponsiveQuery'
12 |
13 | export const resolveBreakpoint = (breakpointRef: BreakpointRef): Breakpoint => {
14 | return typeof breakpointRef === 'string'
15 | ? Layout.breakpoints[breakpointRef]
16 | : breakpointRef
17 | }
18 |
19 | /**
20 | * Returns a list of breakpoints based on a responsive query.
21 | * @example
22 | * getBreakpointsByQuery({ from: 'md' })
23 | * // [{ minWidth: 768 }]
24 | * getBreakpointsByQuery({ from: 'sm', to: 'lg' })
25 | * // [{ minWidth: 576 }, { maxWidth: 1199 }]
26 | */
27 | export const getBreakpointsByQuery = (
28 | params: ResponsiveQueryParams,
29 | ): Breakpoint[] => {
30 | const { for: exactBreakpoint, from, to, except } = params
31 |
32 | // Explicit breakpoint
33 | if (exactBreakpoint) {
34 | return [resolveBreakpoint(exactBreakpoint)]
35 | }
36 |
37 | const minBreakpoint = resolveBreakpoint(from)
38 | const maxBreakpoint = resolveBreakpoint(to)
39 |
40 | // Bell, __/--\__
41 | if (minBreakpoint && maxBreakpoint && !except) {
42 | const mergedAreaRecord = mergeAreaRecords(
43 | {
44 | behavior: 'down',
45 | breakpoint: maxBreakpoint,
46 | },
47 | {
48 | behavior: 'up',
49 | breakpoint: minBreakpoint,
50 | },
51 | false,
52 | )
53 |
54 | return [mergedAreaRecord.breakpoint]
55 | }
56 |
57 | // Notch, --\__/--
58 | if (minBreakpoint && maxBreakpoint && except) {
59 | return [closeBreakpoint(minBreakpoint), openBreakpoint(maxBreakpoint)]
60 | }
61 |
62 | // High-pass, __/--
63 | if (minBreakpoint && !maxBreakpoint) {
64 | return [openBreakpoint(minBreakpoint)]
65 | }
66 |
67 | // Low-pass, --\__
68 | if (!minBreakpoint && maxBreakpoint) {
69 | return [closeBreakpoint(maxBreakpoint)]
70 | }
71 |
72 | return []
73 | }
74 |
--------------------------------------------------------------------------------
/packages/atomic-layout/src/utils/makeResponsive.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Numeric } from '@atomic-layout/core'
3 | import useResponsiveProps from '../hooks/useResponsiveProps'
4 |
5 | /**
6 | * Returns a new React component based on the given one
7 | * that enables support for Responsive Props API on arbitrary props.
8 | */
9 | export function makeResponsive<
10 | OwnProps extends Record,
11 | ResponsiveProps extends Record,
12 | RefType = unknown
13 | >(
14 | Component: React.FC,
15 | ): React.FC>> {
16 | return React.forwardRef>(
17 | (responsiveProps, ref) => {
18 | /**
19 | * @see https://github.com/Microsoft/TypeScript/issues/29049
20 | */
21 | const actualProps = useResponsiveProps(
22 | responsiveProps,
23 | ) as OwnProps & Partial
24 |
25 | return
26 | },
27 | )
28 | }
29 |
--------------------------------------------------------------------------------
/packages/atomic-layout/src/utils/query.test.ts:
--------------------------------------------------------------------------------
1 | import { query } from './query'
2 |
3 | describe('query', () => {
4 | describe('given an exact breakpoint (for)', () => {
5 | let result: ReturnType
6 |
7 | beforeAll(() => {
8 | result = query({ for: 'md' })
9 | })
10 |
11 | it('should return an enclosed media query for the given breakpoint', () => {
12 | expect(result).toEqual('(min-width:768px) and (max-width:991px)')
13 | })
14 | })
15 |
16 | describe('given a high-pass breakpoint range (from)', () => {
17 | let result: ReturnType
18 |
19 | beforeAll(() => {
20 | result = query({ from: 'md' })
21 | })
22 |
23 | it('should return an enclosed media query for the given range', () => {
24 | expect(result).toEqual('(min-width:768px)')
25 | })
26 | })
27 |
28 | describe('given a low-pass breakpoint range (to)', () => {
29 | let result: ReturnType
30 |
31 | beforeAll(() => {
32 | result = query({ to: 'lg' })
33 | })
34 |
35 | it('should return an enclosed media query for the given range', () => {
36 | expect(result).toEqual('(max-width:991px)')
37 | })
38 | })
39 |
40 | describe('given a bell breakpoint range (from/to)', () => {
41 | let result: ReturnType
42 |
43 | beforeAll(() => {
44 | result = query({ from: 'sm', to: 'lg' })
45 | })
46 |
47 | it('should return an enclosed media query for the given range', () => {
48 | expect(result).toEqual(
49 | '(min-width:576px) and (max-width:calc(992px - 1px))',
50 | )
51 | })
52 | })
53 |
54 | describe('given a notch breakpoint range (except/from/to)', () => {
55 | let result: ReturnType
56 |
57 | beforeAll(() => {
58 | result = query({ except: true, from: 'sm', to: 'lg' })
59 | })
60 |
61 | it('should return an enclosed media query for the given range', () => {
62 | expect(result).toEqual('(max-width:575px),(min-width:992px)')
63 | })
64 | })
65 | })
66 |
--------------------------------------------------------------------------------
/packages/atomic-layout/src/utils/query.ts:
--------------------------------------------------------------------------------
1 | import { memoizeWith } from '@atomic-layout/core'
2 | import { createMediaQuery } from '../hooks/useMediaQuery'
3 | import { ResponsiveQueryParams } from '../hooks/useResponsiveQuery'
4 | import { getBreakpointsByQuery } from './getBreakpointsByQuery'
5 |
6 | const createQuery = (params: ResponsiveQueryParams): string => {
7 | const breakpoints = getBreakpointsByQuery(params)
8 | return breakpoints.map(createMediaQuery).join(params.except ? ',' : ' ')
9 | }
10 |
11 | /**
12 | * Converts a responsive query into a @media query string.
13 | * @example
14 | * query({ from: 'md' })
15 | * // (min-width: 768px)
16 | * query({ from: 'sm', to: 'lg' })
17 | * // (min-width: 576px) and (max-width: 1199px)
18 | * query({ for: 'md' })
19 | * // (min-width: 768px) and (max-width: 991px)
20 | * query({ except: true, from: 'sm', to: 'lg' })
21 | * // (max-width: 575px), (min-width: 992px)
22 | */
23 | export const query = memoizeWith((params) => {
24 | return Object.entries(params)
25 | .filter(([, value]) => value != null)
26 | .reduce((acc, [key, value]) => {
27 | return acc.concat(`${key}=${value.toString()}`)
28 | }, [])
29 | .join()
30 | })(createQuery)
31 |
--------------------------------------------------------------------------------
/packages/atomic-layout/src/utils/withPlaceholder.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { Breakpoint, AreaComponent, GenericProps } from '@atomic-layout/core'
3 | import { forwardRef } from './forwardRef'
4 | import { useMediaQuery } from '../hooks/useMediaQuery'
5 |
6 | /**
7 | * Wraps the given area component in a placeholder component.
8 | * This is used for conditional components, where placeholder component is rendered
9 | * until the condition for that area component is met (i.e. viewport matches a breakpoint).
10 | */
11 | export const withPlaceholder = (
12 | Component: AreaComponent,
13 | breakpoints: Breakpoint[],
14 | ) => {
15 | const Placeholder = forwardRef(
16 | ({ children, ...restProps }, ref) => {
17 | const matches = useMediaQuery(breakpoints)
18 | return (
19 | matches && (
20 |
21 | {children}
22 |
23 | )
24 | )
25 | },
26 | )
27 |
28 | Placeholder.displayName = `Placeholder(${Component.displayName})`
29 |
30 | return Placeholder
31 | }
32 |
--------------------------------------------------------------------------------
/packages/atomic-layout/test/createForwardRefTest.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { render } from '@testing-library/react'
3 | import '../../atomic-layout-core/src/utils/breakpoints/withBreakpoints/matchMedia.mock'
4 | import {
5 | Box as DefaultBox,
6 | Composition as DefaultComposition,
7 | Only as DefaultOnly,
8 | Visible as DefaultVisible,
9 | } from '../src/'
10 |
11 | interface ComponentsMap {
12 | Box: typeof DefaultBox
13 | Composition: typeof DefaultComposition
14 | Only: typeof DefaultOnly
15 | Visible: typeof DefaultVisible
16 | }
17 |
18 | export const createForwardRefTest = (components: ComponentsMap) => {
19 | const { Box, Composition, Only, Visible } = components
20 |
21 | describe('Refs', () => {
22 | it('Supports ref forwarding for the "Box" component', () => {
23 | const ref = React.createRef()
24 | render( )
25 | expect(ref.current).toBeInstanceOf(HTMLDivElement)
26 | })
27 |
28 | it('Supports ref forwarding for the "Composition" component', () => {
29 | const ref = React.createRef()
30 | render(
31 |
32 | Arbitrary content
33 | ,
34 | )
35 | expect(ref.current).toBeInstanceOf(HTMLSpanElement)
36 | })
37 |
38 | it('Supports ref forwarding for the generated Area components', () => {
39 | const leftRef = React.createRef()
40 | const rightRef = React.createRef()
41 | render(
42 |
43 | {(Areas) => (
44 | <>
45 | Left
46 |
47 | Right
48 |
49 | >
50 | )}
51 | ,
52 | )
53 | expect(leftRef.current).toBeInstanceOf(HTMLDivElement)
54 | expect(rightRef.current).toBeInstanceOf(HTMLSpanElement)
55 | })
56 |
57 | it('Supports ref forwarding for the "Only" component', () => {
58 | const ref = React.createRef()
59 | render( )
60 | expect(ref.current).toBeInstanceOf(HTMLDivElement)
61 | })
62 |
63 | it('Supports ref forwarding for the "Visible" component', () => {
64 | const ref = React.createRef()
65 | render( )
66 | expect(ref.current).toBeInstanceOf(HTMLSpanElement)
67 | })
68 | })
69 | }
70 |
--------------------------------------------------------------------------------
/packages/atomic-layout/test/createSsrTest.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { renderToString } from 'react-dom/server'
3 |
4 | /**
5 | * Creates a SSR unit test scenario with the given built module.
6 | */
7 | export const createSsrTest = (
8 | getModule: () => Promise,
9 | ): void => {
10 | describe('Server-side rendering', () => {
11 | let library: typeof import('../src/index')
12 |
13 | beforeAll(async () => {
14 | library = await getModule()
15 | })
16 |
17 | it('rendering on a server without crashing', () => {
18 | const { Box, Only, Composition } = library
19 |
20 | const renderOnServer = () =>
21 | renderToString(
22 |
23 |
24 | Responsive content
25 |
26 | {({ First, Second }: any) => (
27 | <>
28 | First
29 | Second
30 | >
31 | )}
32 |
33 | ,
34 | )
35 |
36 | expect(renderOnServer).not.toThrow()
37 | })
38 | })
39 | }
40 |
--------------------------------------------------------------------------------
/packages/atomic-layout/test/propAliases.spec.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { propAliases } from '@atomic-layout/core'
3 | import Composition from '../src/components/Composition'
4 | import { render, cleanup, getByTestId } from '@testing-library/react'
5 | import '@testing-library/jest-dom/extend-expect'
6 |
7 | const defaultValue = 10
8 | const explicitValues: Record<
9 | string,
10 | string | number | Array
11 | > = {
12 | area: 'first',
13 | // prettier-ignore
14 | areas: 'first',
15 | // prettier-ignore
16 | template: "first",
17 | templateCols: '500px',
18 | templateRows: '500px',
19 | col: '1 / auto',
20 | colStart: '2',
21 | colEnd: '3',
22 | gap: '20px 25px',
23 | gutter: '20px 25px',
24 | margin: ['0px', 10],
25 | flexDirection: ['row', 'column'],
26 | flexShrink: ['0', '1'],
27 | flexGrow: ['1', '0'],
28 | flexWrap: ['nowrap', 'wrap', 'wrap-reverse'],
29 | row: '1 / auto',
30 | rowStart: '2',
31 | rowEnd: '3',
32 | autoFlow: ['row', 'column', 'row dense'],
33 | align: 'center',
34 | alignItems: 'flex-end',
35 | alignContent: ['flex-start', 'space-around', 'space-between'],
36 | justify: 'center',
37 | justifyItems: 'flex-end',
38 | justifyContent: 'flex-start',
39 | place: 'center center',
40 | placeItems: 'flex-end flex-end',
41 | placeContent: 'flex-start flex-start',
42 | }
43 |
44 | describe('Prop aliases', () => {
45 | afterEach(cleanup)
46 |
47 | Object.keys(propAliases).forEach((propAliasName) => {
48 | describe(propAliasName, () => {
49 | const propValues: Array = [].concat(
50 | explicitValues[propAliasName] || defaultValue,
51 | )
52 |
53 | propValues.forEach((propValue) => {
54 | describe(`given the value: ${propValue}`, () => {
55 | const props = {
56 | [propAliasName]: propValue,
57 | }
58 | const { container } = render(
59 |
60 | {({ First }) => {propAliasName} }
61 | ,
62 | )
63 | const element = getByTestId(container, 'composition')
64 |
65 | const { props: cssProps, transformValue } = propAliases[propAliasName]
66 | const expectedValue = transformValue
67 | ? transformValue(propValue)
68 | : propValue
69 |
70 | cssProps.forEach((cssPropName) => {
71 | const expectedStylesString = `${cssPropName}:${expectedValue}`
72 |
73 | it(`produces "${expectedStylesString}"`, () => {
74 | expect(element).toHaveStyle(expectedStylesString)
75 | })
76 | })
77 | })
78 | })
79 | })
80 | })
81 | })
82 |
--------------------------------------------------------------------------------
/packages/atomic-layout/test/refs.spec.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Composition, Only, Visible } from '../src'
2 | import { createForwardRefTest } from './createForwardRefTest'
3 |
4 | createForwardRefTest({
5 | Box,
6 | Composition,
7 | Only,
8 | Visible,
9 | })
10 |
--------------------------------------------------------------------------------
/packages/atomic-layout/test/ssr.spec.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * @jest-environment node
3 | */
4 | import { createSsrTest } from './createSsrTest'
5 |
6 | createSsrTest(() => import('../src'))
7 |
--------------------------------------------------------------------------------
/packages/atomic-layout/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": true,
3 | "compilerOptions": {
4 | "module": "es6",
5 | "target": "esnext",
6 | "moduleResolution": "node",
7 | "declaration": true,
8 | "emitDeclarationOnly": true,
9 | "declarationDir": "lib/types",
10 | "noImplicitThis": false,
11 | "noImplicitAny": true,
12 | "esModuleInterop": true,
13 | "jsx": "react",
14 | "baseUrl": ".",
15 | "types": ["jest"]
16 | },
17 | "include": ["src/**/*"],
18 | "exclude": ["node_modules", "**/*.spec.ts"]
19 | }
20 |
--------------------------------------------------------------------------------
/packages/atomic-layout/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["tslint-react", "../../tslint.json"],
3 | "rules": {
4 | "no-console": true,
5 | "no-submodule-imports": false,
6 | "jsx-boolean-value": ["never"],
7 | "jsx-no-multiline-js": false,
8 | "jsx-wrap-multiline": false
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["tslint:latest", "tslint-config-prettier"],
3 | "rules": {
4 | "no-console": false,
5 | "prefer-template": [true, "always"],
6 | "no-bitwise": false,
7 |
8 | "no-submodule-imports": [true, "@testing-library/jest-dom/extend-expect"],
9 | "no-implicit-dependencies": [true, "dev"],
10 | "ordered-imports": [false, "never"],
11 | "object-literal-sort-keys": [false, "never"],
12 | "only-arrow-functions": [false],
13 | "interface-name": [true, "never-prefix"],
14 | "no-object-literal-type-assertion": false
15 | }
16 | }
17 |
--------------------------------------------------------------------------------