├── .eslintignore ├── .env ├── .npmignore ├── .eslintrc ├── src ├── container │ ├── contentfragment │ │ └── v1 │ │ │ ├── ContentFragmentV1IsEmptyFn.ts │ │ │ ├── index.ts │ │ │ ├── model.ts │ │ │ ├── ContentFragmentV1Registry.ts │ │ │ ├── ContentFragmentV1.tsx │ │ │ ├── DefaultContentFragmentV1Renderer.tsx │ │ │ ├── ContentFragmentV1.test.model.json │ │ │ └── ContentFragmentV1.test.tsx │ ├── tabs │ │ └── v1 │ │ │ ├── TabsV1IsEmptyFn.ts │ │ │ ├── TabsV1IsEmptyFn.test.tsx │ │ │ ├── TabsV1.test.tsx │ │ │ └── TabsV1.tsx │ ├── accordion │ │ └── v1 │ │ │ ├── AccordionV1IsEmptyFn.ts │ │ │ ├── AccordionV1IsEmptyFn.test.tsx │ │ │ ├── AccordionV1.tsx │ │ │ └── AccordionV1.test.tsx │ ├── container │ │ └── v1 │ │ │ ├── ContainerV1IsEmptyFn.ts │ │ │ ├── ContainerV1IsEmptyFn.test.tsx │ │ │ ├── ContainerV1.tsx │ │ │ └── ContainerV1.test.tsx │ └── carousel │ │ └── v1 │ │ ├── CarouselV1IsEmptyFn.ts │ │ ├── CarouselV1IsEmptyFn.test.tsx │ │ ├── CarouselV1.test.tsx │ │ └── CarouselV1.tsx ├── tsconfig.types.json ├── TestComponentMapping.d.ts ├── index.test.ts ├── tsconfig.base.json ├── isEmptyFunctions.ts ├── index.ts ├── setupTests.ts ├── TestComponentMapping.tsx └── AbstractCoreContainerComponent.tsx ├── .gitignore ├── TestComponentMapping.d.ts.map ├── .github ├── workflows │ ├── security.yml │ ├── release.yml │ └── ci.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── renovate.json ├── tsconfig.json ├── config ├── webpack.config.core.js ├── webpack.config.lib.js └── webpack.config.base.js ├── jest.config.js ├── README.md ├── package.json └── LICENSE /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | config/ -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | PUBLIC_URL=/etc.clientlibs/core-components-examples/wcm-react/clientlibs/react-webcomponents/resources/ 2 | NODE_PATH=./ 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | config 2 | node 3 | node_modules 4 | .eslintignore 5 | .gitignore 6 | .babelrc 7 | *.iml 8 | jest.config.js 9 | pom.xml 10 | tsconfig.json 11 | src -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["plugin:@typescript-eslint/recommended"], 3 | "rules": { 4 | "@typescript-eslint/ban-ts-comment": "off", 5 | "@typescript-eslint/no-empty-interface": "off" 6 | } 7 | } -------------------------------------------------------------------------------- /src/container/contentfragment/v1/ContentFragmentV1IsEmptyFn.ts: -------------------------------------------------------------------------------- 1 | import {ContentFragmentV1Properties} from "./model"; 2 | 3 | export function ContentFragmentV1IsEmptyFn(props:ContentFragmentV1Properties) { 4 | return (props.elementsOrder == null || props.elementsOrder.length === 0) && (props.cqItemsOrder == null || props.cqItemsOrder.length === 0); 5 | } -------------------------------------------------------------------------------- /src/container/contentfragment/v1/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./ContentFragmentV1IsEmptyFn"; 2 | export * from "./ContentFragmentV1Registry"; 3 | export { default as ContentFragmentRegistryV1 } from "./ContentFragmentV1Registry"; 4 | export * from "./model"; 5 | export * from "./DefaultContentFragmentV1Renderer"; 6 | 7 | export { default as ContentFragmentV1 } from "./ContentFragmentV1"; 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | .idea 3 | .classpath 4 | .metadata 5 | .project 6 | .settings 7 | .externalToolBuilders 8 | maven-eclipse.xml 9 | *.swp 10 | *.iml 11 | *.ipr 12 | *.iws 13 | *.bak 14 | .vlt 15 | .DS_Store 16 | jcr.log 17 | atlassian-ide-plugin.xml 18 | .vlt-sync.log 19 | .vlt-sync-config.properties 20 | node 21 | node_modules 22 | 23 | 24 | src/tsconfig.types.tsbuildinfo 25 | dist 26 | coverage 27 | -------------------------------------------------------------------------------- /src/tsconfig.types.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "composite": true, 5 | "module": "es2015", 6 | "target": "esnext", 7 | "importHelpers": true, 8 | "removeComments": false, 9 | "declaration": true, 10 | "declarationMap": true, 11 | "declarationDir": "../dist", 12 | "emitDeclarationOnly": true 13 | }, 14 | 15 | "exclude": [ 16 | "./internal/umd.ts" 17 | ] 18 | } -------------------------------------------------------------------------------- /TestComponentMapping.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"TestComponentMapping.d.ts","sourceRoot":"","sources":["TestComponentMapping.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAa,yBAAyB,EAAE,gBAAgB,EAAQ,MAAM,qCAAqC,CAAC;AACnH,OAAO,EAAC,KAAK,EAAC,MAAM,kCAAkC,CAAC;AACvD,OAAO,EAAC,iBAAiB,EAAC,MAAM,OAAO,CAAC;AAGxC,MAAM,WAAW,eAAgB,SAAQ,yBAAyB;IAC9D,KAAK,EAAE,MAAM,CAAC;CACjB;AAED,UAAU,SAAU,SAAQ,KAAK,EAAC,iBAAiB,EAAC,iBAAiB;IACjE,KAAK,EAAC,MAAM,CAAC;CAChB;AAED,eAAO,MAAM,UAAU,EAAC;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAA;CAYjD,CAAC;AAcF,eAAe,gBAAgB,CAAC"} -------------------------------------------------------------------------------- /.github/workflows/security.yml: -------------------------------------------------------------------------------- 1 | name: Vulnerability check 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request_target: 7 | 8 | jobs: 9 | security: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout source code 13 | uses: actions/checkout@master 14 | - name: Run Snyk to check for vulnerabilities 15 | uses: snyk/actions/node@master 16 | env: 17 | SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} 18 | with: 19 | command: monitor --all-projects --exclude=examples,pom.xml 20 | -------------------------------------------------------------------------------- /src/TestComponentMapping.d.ts: -------------------------------------------------------------------------------- 1 | import { MappedComponentProperties, ComponentMapping } from "@adobe/cq-react-editable-components"; 2 | import { Model } from "@adobe/cq-spa-page-model-manager"; 3 | import { CoreContainerItem } from "./src"; 4 | export interface DummyProperties extends MappedComponentProperties { 5 | value: string; 6 | } 7 | interface DummyItem extends Model, CoreContainerItem, CoreContainerItem { 8 | value: string; 9 | } 10 | export declare const dummyItems: { 11 | [key: string]: DummyItem; 12 | }; 13 | export default ComponentMapping; 14 | //# sourceMappingURL=TestComponentMapping.d.ts.map -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "inlineSources": true, 10 | "inlineSourceMap": true, 11 | "experimentalDecorators": true, 12 | "allowJs": true, 13 | "skipLibCheck": true, 14 | "esModuleInterop": true, 15 | "allowSyntheticDefaultImports": true, 16 | "strict": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "module": "esnext", 19 | "moduleResolution": "node", 20 | "resolveJsonModule": true, 21 | "isolatedModules": true, 22 | "jsx": "react" 23 | }, 24 | "include": [ 25 | "src", 26 | "src/**/*.json" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[feature] " 5 | labels: feature-request 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /src/container/contentfragment/v1/model.ts: -------------------------------------------------------------------------------- 1 | 2 | import {ContainerProperties, MappedComponentProperties} from "@adobe/aem-react-editable-components"; 3 | import {HasBaseCssClass} from "../../../AbstractCoreContainerComponent"; 4 | 5 | export interface ContentFragmentV1Element { 6 | "dataType": string 7 | "value": string 8 | "title": string 9 | ":type": string 10 | "multiValue": boolean 11 | } 12 | 13 | export interface ContentFragmentV1Properties extends ContainerProperties, MappedComponentProperties, HasBaseCssClass { 14 | 15 | id: string 16 | title: string 17 | description: string, 18 | elements: { [key: string]: ContentFragmentV1Element } 19 | elementsOrder: string[] 20 | model: string 21 | 22 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[bug]" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **Package version** 14 | Provide a package version where the bug occurs. 15 | 16 | **To Reproduce** 17 | Steps to reproduce the behavior: 18 | 1. Go to '...' 19 | 2. Click on '....' 20 | 3. Scroll down to '....' 21 | 4. See error 22 | 23 | **Expected behavior** 24 | A clear and concise description of what you expected to happen. 25 | 26 | **Screenshots** 27 | If applicable, add screenshots to help explain your problem. 28 | 29 | **Additional context** 30 | Add any other context about the problem here. 31 | -------------------------------------------------------------------------------- /src/index.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as SpaCoreComponents from "./index"; 18 | 19 | it('Exports stuff', () => { 20 | 21 | expect(SpaCoreComponents).toBeDefined(); 22 | 23 | }); -------------------------------------------------------------------------------- /src/tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": false, 5 | "baseUrl": ".", 6 | "paths": { 7 | "@adobe/aem-react-editable-components": ["node_modules/@adobe/aem-react-editable-components"], 8 | "@adobe/aem-react-editable-components/*": ["node_modules/@adobe/aem-react-editable-components/*"], 9 | "@adobe/aem-spa-component-mapping": ["node_modules/@adobe/aem-spa-component-mapping"], 10 | "@adobe/aem-spa-component-mapping/*": ["node_modules/@adobe/aem-spa-component-mapping/*"], 11 | "@adobe/aem-spa-page-model-manager": ["node_modules/@adobe/aem-spa-page-model-manager"], 12 | "@adobe/aem-spa-page-model-manager/*": ["node_modules/@adobe/aem-spa-page-model-manager/*"] 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /src/container/tabs/v1/TabsV1IsEmptyFn.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {TabsV1Properties} from "./TabsV1"; 18 | 19 | export function TabsV1IsEmptyFn(props:TabsV1Properties){ 20 | return props.cqItemsOrder == null || props.cqItemsOrder.length === 0; 21 | } -------------------------------------------------------------------------------- /src/container/accordion/v1/AccordionV1IsEmptyFn.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {AccordionV1Properties} from "./AccordionV1"; 18 | 19 | export function AccordionV1IsEmptyFn(props:AccordionV1Properties){ 20 | return props.cqItemsOrder == null || props.cqItemsOrder.length == 0; 21 | } -------------------------------------------------------------------------------- /src/container/container/v1/ContainerV1IsEmptyFn.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {ContainerV1Properties} from "./ContainerV1"; 18 | 19 | export function ContainerV1IsEmptyFn(props:ContainerV1Properties) { 20 | return props.cqItemsOrder == null || props.cqItemsOrder.length === 0; 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: 5 | - master 6 | jobs: 7 | release: 8 | name: Release and publish module 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout source code 12 | uses: actions/checkout@v2 13 | with: 14 | fetch-depth: 0 15 | - name: Setup Node.js 16 | uses: actions/setup-node@v2 17 | with: 18 | node-version: 12 19 | - name: Install dependencies 20 | run: npm ci 21 | - name: Build the project 22 | run: npm run build:production 23 | - name: Run tests and do code coverage check 24 | run: npm run test:coverage 25 | - name: Release module and publish it in github.com and npmjs.com 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | NPM_TOKEN: ${{ secrets.ADOBE_BOT_NPM_TOKEN }} 29 | run: npm run semantic-release 30 | 31 | -------------------------------------------------------------------------------- /src/container/carousel/v1/CarouselV1IsEmptyFn.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import {CoreContainerProperties} from "../../../AbstractCoreContainerComponent"; 17 | import { CarouselV1Properties } from "./CarouselV1"; 18 | 19 | export function CarouselV1IsEmptyFn(props:CarouselV1Properties){ 20 | return props.cqItemsOrder == null || props.cqItemsOrder.length === 0; 21 | } -------------------------------------------------------------------------------- /src/isEmptyFunctions.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export * from "./container/accordion/v1/AccordionV1IsEmptyFn"; 18 | export * from "./container/container/v1/ContainerV1IsEmptyFn"; 19 | export * from "./container/carousel/v1/CarouselV1IsEmptyFn"; 20 | export * from "./container/tabs/v1/TabsV1IsEmptyFn"; 21 | export * from "./container/contentfragment/v1/ContentFragmentV1IsEmptyFn" -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "timezone": "Europe/Zurich", 3 | "masterIssue": true, 4 | "packageRules": [ 5 | { 6 | "groupName": "@adobe fixes", 7 | "updateTypes": ["patch", "pin", "digest", "minor"], 8 | "automerge": true, 9 | "packagePatterns": ["^@adobe/"], 10 | "schedule": ["at any time"] 11 | }, 12 | { 13 | "groupName": "@adobe major", 14 | "updateTypes": ["major"], 15 | "packagePatterns": ["^@adobe/"], 16 | "automerge": false, 17 | "schedule": ["at any time"] 18 | }, 19 | { 20 | "groupName": "external fixes", 21 | "updateTypes": ["patch", "pin", "digest", "minor"], 22 | "automerge": false, 23 | "schedule": ["after 1pm on Monday"], 24 | "packagePatterns": ["^.+"], 25 | "excludePackagePatterns": ["^@adobe/"] 26 | }, 27 | { 28 | "groupName": "external major", 29 | "updateTypes": ["major"], 30 | "automerge": false, 31 | "packagePatterns": ["^.+"], 32 | "excludePackagePatterns": ["^@adobe/"], 33 | "schedule": ["after 1pm on Monday"] 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | on: pull_request_target 3 | 4 | jobs: 5 | test-react-spa-components: 6 | name: test react-spa-components 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout source code 10 | uses: actions/checkout@v2 11 | - name: Setup Node.js 12 | uses: actions/setup-node@v1 13 | with: 14 | node-version: '12' 15 | - name: Install dependencies 16 | run: npm ci 17 | - name: Build the project 18 | run: npm run build:production 19 | - name: Run tests and do code coverage check 20 | run: npm run test:coverage 21 | - name: Run code linter 22 | uses: hallee/eslint-action@1.0.3 23 | if: ${{ github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository }} 24 | with: 25 | repo-token: ${{ secrets.GITHUB_TOKEN }} 26 | - name: Upload code coverage report to workflow as an artifact 27 | uses: actions/upload-artifact@v2 28 | with: 29 | name: spa-coverage.zip 30 | path: coverage 31 | - name: Upload code coverage report to codecov.io and comment in pull request 32 | uses: codecov/codecov-action@v1 33 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | export * from './AbstractCoreContainerComponent'; 17 | export * from './isEmptyFunctions'; 18 | 19 | export * from './container/tabs/v1/TabsV1'; 20 | export { default as TabsV1 } from "./container/tabs/v1/TabsV1"; 21 | 22 | export * from './container/accordion/v1/AccordionV1'; 23 | export { default as AccordionV1 } from "./container/accordion/v1/AccordionV1"; 24 | 25 | export * from './container/carousel/v1/CarouselV1'; 26 | export { default as CarouselV1 } from "./container/carousel/v1/CarouselV1"; 27 | 28 | export * from './container/container/v1/ContainerV1'; 29 | export { default as ContainerV1 } from "./container/container/v1/ContainerV1"; 30 | 31 | export * from './container/contentfragment/v1'; 32 | -------------------------------------------------------------------------------- /config/webpack.config.core.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const DllPlugin = require("webpack").DllPlugin; 18 | const config = require('./webpack.config.base'); 19 | const path = require('path'); 20 | const { CleanWebpackPlugin } = require('clean-webpack-plugin'); 21 | 22 | config.output.library = 'AbstractCoreContainerComponent'; 23 | 24 | config.entry = { 25 | 'AbstractCoreContainerComponent': ['./src/AbstractCoreContainerComponent.tsx'], 26 | }; 27 | 28 | config.plugins.push(new CleanWebpackPlugin()); 29 | config.plugins.push( 30 | new DllPlugin({ 31 | context: path.join(__dirname, '..'), 32 | name: "[name]", 33 | path: path.resolve(__dirname, `./../dist/manifest/[name].json`), 34 | }) 35 | ); 36 | 37 | module.exports = config; -------------------------------------------------------------------------------- /config/webpack.config.lib.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const config = require('./webpack.config.base'); 18 | 19 | config.output.library = '@adobe/aem-core-components-react-spa'; 20 | 21 | config.entry = { 22 | 'index': ['./src/index.ts'], 23 | 'isEmptyFunctions': ['./src/isEmptyFunctions.ts'], 24 | 'container/contentfragment/v1': ['./src/container/contentfragment/v1/index.ts'], 25 | 'container/accordion/v1/AccordionV1': ['./src/container/accordion/v1/AccordionV1.tsx'], 26 | 'container/carousel/v1/CarouselV1': ['./src/container/carousel/v1/CarouselV1.tsx'], 27 | 'container/container/v1/ContainerV1': ['./src/container/container/v1/ContainerV1.tsx'], 28 | 'container/tabs/v1/TabsV1': ['./src/container/tabs/v1/TabsV1.tsx'] 29 | }; 30 | 31 | module.exports = config; 32 | 33 | 34 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | module.exports = { 20 | preset: "ts-jest", 21 | setupFilesAfterEnv: ['/src/setupTests.ts'], 22 | testEnvironment: 'jsdom', 23 | transform: { 24 | "^.+\\.tsx?$": "ts-jest" 25 | }, 26 | testMatch: ['/**/*.test.ts','/**/*.test.tsx'], 27 | testPathIgnorePatterns: [ 28 | 'node_modules', 29 | 'lib', 30 | 'dist', 31 | 'node' 32 | ], 33 | collectCoverageFrom: [ 34 | 'src/**/*.{ts,tsx}' 35 | ], 36 | coveragePathIgnorePatterns: [ 37 | "/node_modules/", 38 | "/lib/", 39 | "/dist/", 40 | "/node/", 41 | "TestComponentMapping.tsx", 42 | "TestComponentMapping.d.ts" 43 | 44 | ], 45 | moduleFileExtensions: [ 46 | "ts", 47 | "tsx", 48 | "js", 49 | "jsx", 50 | "json", 51 | "node" 52 | ], 53 | moduleNameMapper: { 54 | "^@adobe/(.*)$": ["/node_modules/@adobe/$1"] 55 | } 56 | }; -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {configure} from 'enzyme'; 18 | import Adapter from 'enzyme-adapter-react-16'; 19 | 20 | const callbacks: { (message: any): void; } [] = []; 21 | 22 | jest.useFakeTimers(); 23 | 24 | //@ts-ignore 25 | window.Granite = { 26 | author: { 27 | trigger: (path:string, index:number) => { 28 | callbacks.forEach((callback) => callback({ 29 | data: { 30 | id: path, 31 | operation: 'navigate', 32 | index: index 33 | } 34 | })) 35 | }, 36 | 37 | MessageChannel : function() { 38 | 39 | return { 40 | subscribeRequestMessage: (topic:string, callback:(message:any)=>void) => { 41 | callbacks.push(callback) 42 | }, 43 | unsubscribeRequestMessage: (topic:string, callback:(message:any)=>void) => { 44 | const index:number = callbacks.indexOf(callback); 45 | callbacks.splice(index, 1); 46 | } 47 | } 48 | 49 | } 50 | 51 | } 52 | }; 53 | 54 | configure({ adapter: new Adapter()}); 55 | 56 | -------------------------------------------------------------------------------- /src/TestComponentMapping.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import React, {Component} from "react"; 18 | 19 | 20 | import {EditConfig, MappedComponentProperties, ComponentMapping, MapTo} from "@adobe/aem-react-editable-components"; 21 | 22 | import {CoreContainerItem} from "./AbstractCoreContainerComponent"; 23 | 24 | 25 | export interface DummyProperties extends MappedComponentProperties{ 26 | value: string; 27 | } 28 | 29 | 30 | export const dummyItems:{ [key: string]: CoreContainerItem } = { 31 | "test": { 32 | ":type": "core/components/dummy", 33 | "cq:panelTitle": "Item1", 34 | //@ts-ignore 35 | "value": "Component1", 36 | }, 37 | "test2": { 38 | ":type": "core/components/dummy", 39 | "cq:panelTitle": "Item2", 40 | //@ts-ignore 41 | "value": "Component2" 42 | } 43 | }; 44 | 45 | class DummyCoreComponent extends Component { 46 | render(){ 47 | return
{this.props.value}
48 | } 49 | } 50 | 51 | const editConfig:EditConfig = { 52 | isEmpty: (props: DummyProperties) => !!props.value && props.value.trim().length > 0 53 | }; 54 | 55 | MapTo("core/components/dummy")(DummyCoreComponent, editConfig); 56 | 57 | export default ComponentMapping; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AEM WCM Components - Spa editor - React Core implementation 2 | 3 | This module provides a React implementation for the containers in the [AEM core components](https://www.aemcomponents.dev/). 4 | This only works with the [AEM SPA editor](https://docs.adobe.com/content/help/en/experience-manager-64/developing/headless/spas/spa-overview.html). 5 | 6 | [Introduction Video and Demo](https://www.youtube.com/watch?v=9759AhM7fAc) 7 | 8 | Current supported / exported components: 9 | 10 | ### Containers 11 | - Accordion (V1) 12 | - Carousel (V1) 13 | - Container (V1) 14 | - Tabs (V1) 15 | 16 | ### Abstraction 17 | - AbstractCoreContainerComponent 18 | 19 | ## Usage 20 | 21 | You can choose to import the entire library at once OR import components individually. 22 | The latter is useful if you want to only enable a few components and you want to save your javascript footprint. 23 | Also, if you want to load all core components, but you want to lazyload them with react suspense, you will need to import them individually. 24 | 25 | ### Importing the whole library: 26 | 27 | ``` 28 | import * as SpaCoreComponents from "@adobe/aem-core-components-react-spa"; 29 | const {CarouselV1, CarouselV1IsEmptyFn} = BaseCoreComponents; 30 | ``` 31 | 32 | ### Importing the CarouselV1 component individually: 33 | 34 | ``` 35 | import {CarouselV1, CarouselV1IsEmptyFn} from "@adobe/aem-core-components-react-spa/dist/container/carousel/v1/CarouselV1"; 36 | ``` 37 | 38 | ### Using the imported code 39 | 40 | Now that you have the CarouselV1 and CarouselV1IsEmptyFn imported, you can use them in your project. 41 | The properties of the Button 1 on 1 correspond to the Sling Model Exporter (.model.json) output. 42 | 43 | Note: There are some exceptions where some extra properties are added (mainly i18n labels) that are currently not present in the OOTB sling model exports. 44 | These can be added by the project itself with delegation. If they are not present, the default (English) values will be used. 45 | 46 | #### Carousel - Example with the spa editor: 47 | 48 | ``` 49 | MapTo('my-project/wcm/components/containers')(CarouselV1, {isEmpty: CarouselV1IsEmptyFn}); 50 | ``` 51 | 52 | For a complete project with examples, visit the [github page](https://github.com/adobe/aem-react-core-wcm-components/tree/master/examples). -------------------------------------------------------------------------------- /src/container/contentfragment/v1/ContentFragmentV1Registry.ts: -------------------------------------------------------------------------------- 1 | import {ComponentType} from "react"; 2 | import {ContentFragmentV1Properties} from "./model"; 3 | import {ComponentMapping, MapTo} from "@adobe/aem-react-editable-components"; 4 | 5 | export interface ContentFragmentV1Registry { 6 | mapRenderer: (models: string | string[], component:ComponentType) => void 7 | getRenderer (model:string) : ComponentType 8 | } 9 | 10 | const MODEL_PREFIX = "ContentFragmentV1-" 11 | 12 | class ContentFragmentRegistryV1Impl implements ContentFragmentV1Registry{ 13 | 14 | public mapRenderer(resourceTypes: string | string[], clazz: ComponentType): void { 15 | ContentFragmentRegistryV1Impl.mapRenderer(resourceTypes, clazz); 16 | } 17 | 18 | public static mapRenderer(resourceTypes: string | string[], clazz: ComponentType): void { 19 | if (resourceTypes && clazz) { 20 | const resourceList = (typeof resourceTypes === 'string') ? [ resourceTypes ] : resourceTypes; 21 | 22 | resourceList.forEach((entry) => { 23 | MapTo( this.getModelKey(entry) )( clazz); 24 | }); 25 | } 26 | } 27 | 28 | 29 | 30 | public getRenderer(model: string): ComponentType { 31 | return ContentFragmentRegistryV1Impl.getRenderer(model); 32 | } 33 | 34 | public static getRenderer(model: string): ComponentType { 35 | const modelKey:string = this.getModelKey(model); 36 | return ComponentMapping.get>(modelKey); 37 | } 38 | 39 | private static getModelKey(modelType:string){ 40 | return MODEL_PREFIX + modelType; 41 | } 42 | 43 | } 44 | 45 | export default new ContentFragmentRegistryV1Impl(); 46 | 47 | export const MapToContentFragmentModel = (models: string | string[]): (clazz: ComponentType) => void => { 48 | return (clazz: ComponentType) => ContentFragmentRegistryV1Impl.mapRenderer(models, clazz); 49 | }; 50 | 51 | -------------------------------------------------------------------------------- /src/container/contentfragment/v1/ContentFragmentV1.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import React from "react"; 18 | 19 | import {ComponentMapping, Container} from '@adobe/aem-react-editable-components'; 20 | import {ContentFragmentV1IsEmptyFn} from "./ContentFragmentV1IsEmptyFn"; 21 | import {ContentFragmentV1Properties} from "./model"; 22 | import registry from "./ContentFragmentV1Registry"; 23 | import {DefaultContentFragmentV1Renderer} from "./DefaultContentFragmentV1Renderer"; 24 | import {CoreContainerState, withStandardBaseCssClass} from "../../../AbstractCoreContainerComponent"; 25 | 26 | 27 | class ContentFragmentV1Impl extends Container { 28 | 29 | constructor(props: ContentFragmentV1Properties) { 30 | super(props); 31 | 32 | this.state = { 33 | componentMapping: this.props.componentMapping || ComponentMapping, 34 | }; 35 | 36 | } 37 | 38 | get renderEmptyPlaceHolder() :JSX.Element{ 39 | if(this.props.isInEditor){ 40 | return
ContentFragment is empty.
; 41 | } 42 | return <>; 43 | } 44 | 45 | render() { 46 | 47 | const isEmpty = ContentFragmentV1IsEmptyFn(this.props); 48 | 49 | if(isEmpty){ 50 | return this.renderEmptyPlaceHolder; 51 | } 52 | 53 | let Component = registry.getRenderer(this.props.model); 54 | 55 | if(!Component){ 56 | Component = withStandardBaseCssClass(DefaultContentFragmentV1Renderer, "cmp-contentfragment"); 57 | } 58 | 59 | return (); 60 | 61 | } 62 | 63 | } 64 | 65 | 66 | export default ContentFragmentV1Impl; 67 | -------------------------------------------------------------------------------- /config/webpack.config.base.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const path = require('path'); 18 | const nodeExternals = require('webpack-node-externals'); 19 | 20 | const isEnvironmentTest = process.env.NODE_ENV === 'test'; 21 | const isEnvironmentProd = process.env.NODE_ENV === 'production'; 22 | const { CleanWebpackPlugin } = require('clean-webpack-plugin'); 23 | 24 | const mode = (isEnvironmentProd) ? 'production' : 'development'; 25 | const ManifestPlugin = require('webpack-manifest-plugin'); 26 | 27 | module.exports = { 28 | mode: mode, 29 | devtool: 'source-map', 30 | output: { 31 | globalObject: `typeof self !== 'undefined' ? self : this`, 32 | path: path.resolve(__dirname, '../dist'), 33 | filename: '[name].js', 34 | libraryTarget: 'umd' 35 | }, 36 | module: { 37 | rules: [ 38 | { 39 | test: /\.ts$|\.tsx$/, 40 | exclude: /(node_modules|dist)/, 41 | use: 'ts-loader', 42 | enforce: 'post', 43 | }].concat(isEnvironmentTest ? 44 | { 45 | test: /\.ts$|\.tsx$/, 46 | include: path.resolve(__dirname, 'src'), 47 | use: { 48 | loader: 'istanbul-instrumenter-loader', 49 | options: { 50 | esModules: true, 51 | presets: ["env", "react", "stage-2"] 52 | } 53 | }, 54 | enforce: 'post' 55 | } : []) 56 | }, 57 | externals: [!isEnvironmentTest ? nodeExternals({ 58 | modulesFromFile: { 59 | exclude: ['dependencies'] 60 | } 61 | }) : ''], 62 | resolve: { 63 | extensions: ['.ts', '.tsx'] 64 | }, 65 | plugins: [ 66 | new CleanWebpackPlugin(), 67 | new ManifestPlugin() 68 | ] 69 | }; 70 | -------------------------------------------------------------------------------- /src/container/contentfragment/v1/DefaultContentFragmentV1Renderer.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {Container, ContainerState, MappedComponentProperties, Utils} from "@adobe/aem-react-editable-components"; 3 | import {ContentFragmentV1Element, ContentFragmentV1Properties} from "./model"; 4 | 5 | export class DefaultContentFragmentV1Renderer extends Container { 6 | 7 | protected renderElement(element:ContentFragmentV1Element, name:string, index:number):JSX.Element{ 8 | return ( 9 |
10 |

{name}

11 |
value: {element.value}
12 |
datatype: {element.dataType}
13 |
multivalue: {element.multiValue ? 'true': 'false'}
14 |
type: {element[":type"]}
15 |
16 | ) 17 | } 18 | 19 | protected get elements():JSX.Element{ 20 | return ( 21 |
22 | {this.props.elementsOrder.map((element, index) => this.renderElement(this.props.elements[element], element,index))}; 23 |
24 | ) 25 | } 26 | 27 | render() { 28 | return ( 29 |
30 | 31 |

{this.props.title}

32 | { 33 | !!this.props.description && this.props.description.length > 0 && ( 34 |

{this.props.description}

35 | ) 36 | } 37 | 38 | {this.props.elementsOrder && this.props.elementsOrder.length && this.elements} 39 | 40 | {this.childComponents} 41 | 42 |
43 | ) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/container/tabs/v1/TabsV1IsEmptyFn.test.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as React from 'react'; 18 | import {TabsV1Properties} from "./TabsV1"; 19 | 20 | import ComponentMapping, {dummyItems} from "../../../TestComponentMapping"; 21 | import {TabsV1IsEmptyFn} from "./TabsV1IsEmptyFn"; 22 | import { AllowedComponents, AllowedComponent } from '@adobe/aem-react-editable-components'; 23 | 24 | const GRID_CLASS_NAMES = 'grid-class-names'; 25 | const COLUMN_1_CLASS_NAMES = 'column-class-names-1'; 26 | const COLUMN_2_CLASS_NAMES = 'column-class-names-2'; 27 | 28 | 29 | const allowedComponent:AllowedComponent = { 30 | path: "/apps/core/components/dummy", 31 | title: "Some Component" 32 | }; 33 | 34 | const allowedComponents:AllowedComponents = { 35 | applicable: true, 36 | components: [allowedComponent] 37 | }; 38 | const filledProps:TabsV1Properties = { 39 | accessibilityLabel: 'Tabs', 40 | title: "The Tabs", 41 | cqItems: dummyItems, 42 | cqItemsOrder: ["test", "test2"], 43 | isInEditor: false, 44 | cqPath: "/content/accordion-path", 45 | allowedComponents: allowedComponents, 46 | componentMapping: ComponentMapping 47 | }; 48 | 49 | const emptyProps1:TabsV1Properties = { 50 | accessibilityLabel: 'Tabs', 51 | title: "The Tabs", 52 | cqItems: {}, 53 | cqItemsOrder: [], 54 | isInEditor: false, 55 | cqPath: "/content/accordion-path", 56 | allowedComponents: allowedComponents, 57 | componentMapping: ComponentMapping 58 | }; 59 | 60 | const emptyProps2:TabsV1Properties = { 61 | accessibilityLabel: 'Tabs', 62 | title: "The Tabs", 63 | cqItems: {}, 64 | //@ts-ignore 65 | cqItemsOrder: null, 66 | isInEditor: false, 67 | cqPath: "/content/accordion-path", 68 | allowedComponents: allowedComponents, 69 | componentMapping: ComponentMapping 70 | }; 71 | 72 | 73 | it('Is not empty', () => { 74 | const isEmpty = TabsV1IsEmptyFn(filledProps); 75 | 76 | expect(isEmpty).toEqual(false); 77 | }); 78 | 79 | it('Is empty 1', () => { 80 | const isEmpty = TabsV1IsEmptyFn(emptyProps1); 81 | 82 | expect(isEmpty).toEqual(true); 83 | }); 84 | 85 | 86 | it('Is empty 2', () => { 87 | const isEmpty = TabsV1IsEmptyFn(emptyProps2); 88 | 89 | expect(isEmpty).toEqual(true); 90 | }); -------------------------------------------------------------------------------- /src/container/accordion/v1/AccordionV1IsEmptyFn.test.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as React from 'react'; 18 | import {AccordionV1Properties} from "./AccordionV1"; 19 | 20 | import ComponentMapping, {dummyItems} from "../../../TestComponentMapping"; 21 | import {AccordionV1IsEmptyFn} from "./AccordionV1IsEmptyFn"; 22 | import { AllowedComponents, AllowedComponent } from '@adobe/aem-react-editable-components'; 23 | 24 | const allowedComponent:AllowedComponent = { 25 | path: "/apps/core/components/dummy", 26 | title: "Some Component" 27 | }; 28 | 29 | const allowedComponents:AllowedComponents = { 30 | applicable: true, 31 | components: [allowedComponent] 32 | }; 33 | const filledProps:AccordionV1Properties = { 34 | expandedItems: ["test"], 35 | headingElement: "h2", 36 | singleExpansion: false, 37 | cqItems: dummyItems, 38 | cqItemsOrder: ["test", "test2"], 39 | title: "Accordion", 40 | isInEditor: false, 41 | cqPath: "/content/accordion-path", 42 | allowedComponents: allowedComponents, 43 | componentMapping: ComponentMapping 44 | }; 45 | 46 | const emptyProps1:AccordionV1Properties = { 47 | expandedItems: ["test"], 48 | headingElement: "h2", 49 | singleExpansion: false, 50 | cqItems: {}, 51 | cqItemsOrder: [], 52 | title: "Accordion", 53 | isInEditor: false, 54 | cqPath: "/content/accordion-path", 55 | allowedComponents: allowedComponents, 56 | componentMapping: ComponentMapping 57 | }; 58 | 59 | const emptyProps2:AccordionV1Properties = { 60 | expandedItems: ["test"], 61 | headingElement: "h2", 62 | singleExpansion: false, 63 | cqItems: {}, 64 | //@ts-ignore 65 | cqItemsOrder: null, 66 | title: "Accordion", 67 | isInEditor: false, 68 | cqPath: "/content/accordion-path", 69 | allowedComponents: allowedComponents, 70 | componentMapping: ComponentMapping 71 | }; 72 | 73 | 74 | it('Is not empty', () => { 75 | const isEmpty = AccordionV1IsEmptyFn(filledProps); 76 | 77 | expect(isEmpty).toEqual(false); 78 | }); 79 | 80 | it('Is empty 1', () => { 81 | const isEmpty = AccordionV1IsEmptyFn(emptyProps1); 82 | 83 | expect(isEmpty).toEqual(true); 84 | }); 85 | 86 | 87 | it('Is empty 2', () => { 88 | const isEmpty = AccordionV1IsEmptyFn(emptyProps2); 89 | 90 | expect(isEmpty).toEqual(true); 91 | }); -------------------------------------------------------------------------------- /src/container/contentfragment/v1/ContentFragmentV1.test.model.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "content-fragment-449f30249d", 3 | "title": "niektest", 4 | "description": "", 5 | "cqItems": { 6 | "custom-content-fragment": { 7 | "id": "content-fragment-36fdd89c42", 8 | "title": "niektest", 9 | "description": "", 10 | ":items": {}, 11 | ":itemsOrder": [], 12 | ":type": "core-components-examples/wcm-react/components/content-fragment", 13 | "elements": { 14 | "name": { 15 | "dataType": "string", 16 | "value": "ChildCF", 17 | "title": "First name", 18 | ":type": "string", 19 | "multiValue": false 20 | }, 21 | "age": { 22 | "dataType": "long", 23 | "value": 18, 24 | "title": "Age", 25 | ":type": "long", 26 | "multiValue": false 27 | }, 28 | "blabla": { 29 | "dataType": "string", 30 | "value": "

asddsdsa

\n

asdadadsads

\n", 31 | "title": "blabla", 32 | ":type": "text/html", 33 | "multiValue": false 34 | }, 35 | "testtest": { 36 | "dataType": "json", 37 | "value": "{\"bla\":\"asd\"}", 38 | "title": "testtest", 39 | ":type": "json", 40 | "multiValue": false 41 | }, 42 | "dropdownField": { 43 | "dataType": "string", 44 | "value": "b", 45 | "title": "Dropdown Field", 46 | ":type": "string", 47 | "multiValue": false 48 | } 49 | }, 50 | "elementsOrder": [ 51 | "name", 52 | "age", 53 | "blabla", 54 | "testtest", 55 | "dropdownField" 56 | ], 57 | "model": "testing-frontend-react/models/custom-model" 58 | } 59 | }, 60 | "cqItemsOrder": [ 61 | "custom-content-fragment" 62 | ], 63 | "cqType": "core-components-examples/wcm-react/components/content-fragment", 64 | "elements": { 65 | "name": { 66 | "dataType": "string", 67 | "value": "ChildCF", 68 | "title": "First name", 69 | ":type": "string", 70 | "multiValue": false 71 | }, 72 | "age": { 73 | "dataType": "long", 74 | "value": 18, 75 | "title": "Age", 76 | ":type": "long", 77 | "multiValue": false 78 | }, 79 | "blabla": { 80 | "dataType": "string", 81 | "value": "

asddsdsa

\n

asdadadsads

\n", 82 | "title": "blabla", 83 | ":type": "text/html", 84 | "multiValue": false 85 | }, 86 | "testtest": { 87 | "dataType": "json", 88 | "value": "{\"bla\":\"asd\"}", 89 | "title": "testtest", 90 | ":type": "json", 91 | "multiValue": false 92 | }, 93 | "dropdownField": { 94 | "dataType": "string", 95 | "value": "b", 96 | "title": "Dropdown Field", 97 | ":type": "string", 98 | "multiValue": false 99 | } 100 | }, 101 | "elementsOrder": [ 102 | "name", 103 | "age", 104 | "blabla", 105 | "testtest", 106 | "dropdownField" 107 | ], 108 | "model": "testing-frontend-react/models/testmodel" 109 | } -------------------------------------------------------------------------------- /src/container/container/v1/ContainerV1IsEmptyFn.test.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as React from 'react'; 18 | import {ContainerV1Properties} from "./ContainerV1"; 19 | 20 | import ComponentMapping, {dummyItems} from "../../../TestComponentMapping"; 21 | import {ContainerV1IsEmptyFn} from "./ContainerV1IsEmptyFn"; 22 | import { AllowedComponents, AllowedComponent } from '@adobe/aem-react-editable-components'; 23 | 24 | const GRID_CLASS_NAMES = 'grid-class-names'; 25 | const COLUMN_1_CLASS_NAMES = 'column-class-names-1'; 26 | const COLUMN_2_CLASS_NAMES = 'column-class-names-2'; 27 | 28 | 29 | const allowedComponent:AllowedComponent = { 30 | path: "/apps/core/components/dummy", 31 | title: "Some Component" 32 | }; 33 | 34 | const allowedComponents:AllowedComponents = { 35 | applicable: true, 36 | components: [allowedComponent] 37 | }; 38 | const filledProps:ContainerV1Properties = { 39 | id: "myid", 40 | title: "The Container", 41 | cqItems: dummyItems, 42 | cqItemsOrder: ["test", "test2"], 43 | layout: 'responsiveGrid', 44 | backgroundStyle: "background-color:red", 45 | isInEditor: false, 46 | cqPath: "/content/accordion-path", 47 | columnClassNames: { 48 | 'test': COLUMN_1_CLASS_NAMES, 49 | 'test2': COLUMN_2_CLASS_NAMES 50 | }, 51 | gridClassNames: GRID_CLASS_NAMES, 52 | allowedComponents: allowedComponents, 53 | componentMapping: ComponentMapping 54 | }; 55 | 56 | const emptyProps1:ContainerV1Properties = { 57 | id: "myid", 58 | title: "The Container", 59 | cqItems: {}, 60 | cqItemsOrder: [], 61 | layout: 'responsiveGrid', 62 | backgroundStyle: "background-color:red", 63 | isInEditor: false, 64 | cqPath: "/content/accordion-path", 65 | columnClassNames: { 66 | 'test': COLUMN_1_CLASS_NAMES, 67 | 'test2': COLUMN_2_CLASS_NAMES 68 | }, 69 | gridClassNames: GRID_CLASS_NAMES, 70 | allowedComponents: allowedComponents, 71 | componentMapping: ComponentMapping 72 | }; 73 | 74 | const emptyProps2:ContainerV1Properties = { 75 | accessibilityLabel: 'Container', 76 | accessibility: { 77 | play: 'Play', 78 | pause: 'Pause', 79 | next: 'Next', 80 | previous: 'Previous', 81 | slide: 'Slide {0} of {1}', 82 | indicator: 'Slide %{0}', 83 | indicators: 'Choose a slide to display' 84 | }, 85 | autopauseDisabled: false, 86 | autoplay: true, 87 | delay: 100, 88 | cqItems: {}, 89 | //@ts-ignore 90 | cqItemsOrder: null, 91 | title: "Accordion", 92 | isInEditor: false, 93 | cqPath: "/content/accordion-path", 94 | allowedComponents: allowedComponents, 95 | componentMapping: ComponentMapping 96 | }; 97 | 98 | 99 | it('Is not empty', () => { 100 | const isEmpty = ContainerV1IsEmptyFn(filledProps); 101 | 102 | expect(isEmpty).toEqual(false); 103 | }); 104 | 105 | it('Is empty 1', () => { 106 | const isEmpty = ContainerV1IsEmptyFn(emptyProps1); 107 | 108 | expect(isEmpty).toEqual(true); 109 | }); 110 | 111 | 112 | it('Is empty 2', () => { 113 | const isEmpty = ContainerV1IsEmptyFn(emptyProps2); 114 | 115 | expect(isEmpty).toEqual(true); 116 | }); -------------------------------------------------------------------------------- /src/container/carousel/v1/CarouselV1IsEmptyFn.test.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as React from 'react'; 18 | import {CarouselV1Properties} from "./CarouselV1"; 19 | 20 | import ComponentMapping, {dummyItems} from "../../../TestComponentMapping"; 21 | import {CarouselV1IsEmptyFn} from "./CarouselV1IsEmptyFn"; 22 | import { AllowedComponents, AllowedComponent } from '@adobe/aem-react-editable-components'; 23 | 24 | const allowedComponent:AllowedComponent = { 25 | path: "/apps/core/components/dummy", 26 | title: "Some Component" 27 | }; 28 | 29 | const allowedComponents:AllowedComponents = { 30 | applicable: true, 31 | components: [allowedComponent] 32 | }; 33 | const filledProps:CarouselV1Properties = { 34 | accessibilityLabel: 'Carousel', 35 | accessibility: { 36 | play: 'Play', 37 | pause: 'Pause', 38 | next: 'Next', 39 | previous: 'Previous', 40 | slide: 'Slide {0} of {1}', 41 | indicator: 'Slide %{0}', 42 | indicators: 'Choose a slide to display' 43 | }, 44 | autopauseDisabled: false, 45 | autoplay: true, 46 | delay: 100, 47 | cqItems: dummyItems, 48 | cqItemsOrder: ["test", "test2"], 49 | title: "Accordion", 50 | isInEditor: false, 51 | cqPath: "/content/accordion-path", 52 | allowedComponents: allowedComponents, 53 | componentMapping: ComponentMapping 54 | }; 55 | 56 | const emptyProps1:CarouselV1Properties = { 57 | accessibilityLabel: 'Carousel', 58 | accessibility: { 59 | play: 'Play', 60 | pause: 'Pause', 61 | next: 'Next', 62 | previous: 'Previous', 63 | slide: 'Slide {0} of {1}', 64 | indicator: 'Slide %{0}', 65 | indicators: 'Choose a slide to display' 66 | }, 67 | autopauseDisabled: false, 68 | autoplay: true, 69 | delay: 100, 70 | cqItems: {}, 71 | cqItemsOrder: [], 72 | title: "Accordion", 73 | isInEditor: false, 74 | cqPath: "/content/accordion-path", 75 | allowedComponents: allowedComponents, 76 | componentMapping: ComponentMapping 77 | }; 78 | 79 | const emptyProps2:CarouselV1Properties = { 80 | accessibilityLabel: 'Carousel', 81 | accessibility: { 82 | play: 'Play', 83 | pause: 'Pause', 84 | next: 'Next', 85 | previous: 'Previous', 86 | slide: 'Slide {0} of {1}', 87 | indicator: 'Slide %{0}', 88 | indicators: 'Choose a slide to display' 89 | }, 90 | autopauseDisabled: false, 91 | autoplay: true, 92 | delay: 100, 93 | cqItems: {}, 94 | //@ts-ignore 95 | cqItemsOrder: null, 96 | title: "Accordion", 97 | isInEditor: false, 98 | cqPath: "/content/accordion-path", 99 | allowedComponents: allowedComponents, 100 | componentMapping: ComponentMapping 101 | }; 102 | 103 | 104 | it('Is not empty', () => { 105 | const isEmpty = CarouselV1IsEmptyFn(filledProps); 106 | 107 | expect(isEmpty).toEqual(false); 108 | }); 109 | 110 | it('Is empty 1', () => { 111 | const isEmpty = CarouselV1IsEmptyFn(emptyProps1); 112 | 113 | expect(isEmpty).toEqual(true); 114 | }); 115 | 116 | 117 | it('Is empty 2', () => { 118 | const isEmpty = CarouselV1IsEmptyFn(emptyProps2); 119 | 120 | expect(isEmpty).toEqual(true); 121 | }); -------------------------------------------------------------------------------- /src/AbstractCoreContainerComponent.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as React from 'react'; 18 | 19 | import {ContainerState, AllowedComponentsProperties} from '@adobe/aem-react-editable-components'; 20 | import {ComponentType} from "react"; 21 | import { Model } from '@adobe/aem-spa-page-model-manager'; 22 | 23 | export interface HasBaseCssClass{ 24 | baseCssClass?:string; 25 | } 26 | 27 | export interface CoreContainerProperties extends AllowedComponentsProperties, HasBaseCssClass{ 28 | activeIndexFromAuthorPanel?:number 29 | } 30 | 31 | export interface CoreContainerItem extends Model { 32 | 'cq:panelTitle': string 33 | } 34 | 35 | export interface CoreContainerState extends ContainerState { 36 | 37 | } 38 | 39 | export interface AuthorPanelSwitchState { 40 | activeIndexFromAuthorPanel?: number 41 | } 42 | 43 | const isBrowser = (() => { 44 | try{ 45 | return typeof window !== 'undefined'; 46 | }catch(err){ 47 | return false; 48 | } 49 | })(); 50 | 51 | 52 | export const withStandardBaseCssClass = 53 | ( 54 | Component:ComponentType, 55 | defaultBaseCssClass:string 56 | ):React.ComponentType => { 57 | return (props:M) => { 58 | 59 | const baseCssClass = props.baseCssClass; 60 | const toBeUsedCssClass = baseCssClass && baseCssClass.trim().length > 0 ? baseCssClass : defaultBaseCssClass; 61 | 62 | const mergedProps: M= { 63 | ...props, 64 | baseCssClass: toBeUsedCssClass 65 | }; 66 | 67 | return ; 68 | } 69 | }; 70 | 71 | 72 | 73 | export const withAuthorPanelSwitch = ( 74 | Component:ComponentType 75 | ):React.ComponentType => { 76 | 77 | return class extends React.Component { 78 | 79 | //@ts-ignore 80 | messageChannel; 81 | 82 | constructor(props:M) { 83 | super(props); 84 | this.state = {} 85 | 86 | //@ts-ignore 87 | if (isBrowser && window.Granite && window.Granite.author && window.Granite.author.MessageChannel) { 88 | //@ts-ignore 89 | this.messageChannel = new window.Granite.author.MessageChannel("cqauthor", window); 90 | this.callback = this.callback.bind(this); 91 | } 92 | } 93 | 94 | callback(message:any){ 95 | if (message.data && message.data.id === this.props.cqPath) { 96 | if (message.data.operation === "navigate") { 97 | const index = message.data.index as number; 98 | this.setState({ 99 | activeIndexFromAuthorPanel: index 100 | }) 101 | } 102 | } 103 | } 104 | 105 | componentDidMount(): void { 106 | if(this.messageChannel){ 107 | this.messageChannel.subscribeRequestMessage("cmp.panelcontainer", this.callback); 108 | } 109 | } 110 | 111 | componentWillUnmount(): void { 112 | if(this.messageChannel){ 113 | this.messageChannel.unsubscribeRequestMessage("cmp.panelcontainer", this.callback); 114 | } 115 | } 116 | 117 | 118 | render(){ 119 | return ; 120 | } 121 | } 122 | 123 | }; 124 | -------------------------------------------------------------------------------- /src/container/container/v1/ContainerV1.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import React, {RefObject} from "react"; 18 | 19 | import { 20 | ComponentMapping, 21 | AllowedComponentsContainer, 22 | Container, 23 | ResponsiveGrid, 24 | ResponsiveGridProperties 25 | } from '@adobe/aem-react-editable-components'; 26 | import { 27 | CoreContainerProperties, 28 | CoreContainerState, 29 | withStandardBaseCssClass 30 | } from '../../../AbstractCoreContainerComponent'; 31 | 32 | export interface ContainerV1Properties extends CoreContainerProperties, ResponsiveGridProperties { 33 | backgroundStyle: string; 34 | id: string; 35 | layout?: 'responsiveGrid' | 'simple'; 36 | } 37 | 38 | 39 | class ContainerV1Impl extends AllowedComponentsContainer { 40 | 41 | mainDiv: RefObject; 42 | 43 | constructor(props: ContainerV1Properties) { 44 | super(props); 45 | 46 | //@ts-ignore 47 | this.state = { 48 | componentMapping: this.props.componentMapping || ComponentMapping, 49 | }; 50 | this.mainDiv = React.createRef(); 51 | } 52 | 53 | componentDidMount() { 54 | if (this.mainDiv.current) { 55 | this.mainDiv.current.setAttribute('style', this.props.backgroundStyle); 56 | } 57 | 58 | } 59 | 60 | componentDidUpdate() { 61 | if (this.mainDiv.current) { 62 | this.mainDiv.current.setAttribute('style', this.props.backgroundStyle); 63 | } 64 | } 65 | 66 | get coreContainerProps() { 67 | return { 68 | className: 'container responsivegrid' 69 | }; 70 | } 71 | 72 | 73 | render() { 74 | const { 75 | componentMapping, 76 | allowedComponents, 77 | children, 78 | cqPath, 79 | cqItems, 80 | cqItemsOrder, 81 | isInEditor, 82 | ...otherProps 83 | } = this.props; 84 | 85 | if (isInEditor && allowedComponents && allowedComponents.applicable) { 86 | return super.render(); 87 | } 88 | 89 | const gridProps: ResponsiveGridProperties = { 90 | allowedComponents: {applicable: false, components: []}, 91 | componentMapping: this.state.componentMapping, 92 | gridClassNames: this.props.gridClassNames, 93 | columnClassNames: this.props.columnClassNames, 94 | cqItems: this.props.cqItems, 95 | cqItemsOrder: this.props.cqItemsOrder, 96 | title: "", 97 | cqPath: this.props.cqPath, 98 | isInEditor: false 99 | }; 100 | 101 | return ( 102 |
103 |
106 | 107 | {(this.props.layout && this.props.layout === 'simple') && 108 | } 115 | 116 | {(!this.props.layout || this.props.layout !== 'simple') && 117 | 118 | 119 | } 120 | {this.placeholderComponent} 121 |
122 |
123 | ) 124 | } 125 | 126 | } 127 | 128 | export default withStandardBaseCssClass(ContainerV1Impl, "cmp-container"); 129 | -------------------------------------------------------------------------------- /src/container/container/v1/ContainerV1.test.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as React from 'react'; 18 | import ContainerV1 , {ContainerV1Properties} from "./ContainerV1"; 19 | import {mount} from "enzyme"; 20 | import ReactDOM from 'react-dom'; 21 | 22 | import ComponentMapping, {dummyItems} from "../../../TestComponentMapping"; 23 | import {ModelManager} from "@adobe/aem-spa-page-model-manager" 24 | import {AllowedComponents, AllowedComponent} from "@adobe/aem-react-editable-components" 25 | 26 | const allowedComponent:AllowedComponent = { 27 | path: "/apps/core/components/dummy", 28 | title: "Some Component" 29 | }; 30 | 31 | const GRID_CLASS_NAMES = 'grid-class-names'; 32 | const COLUMN_1_CLASS_NAMES = 'column-class-names-1'; 33 | const COLUMN_2_CLASS_NAMES = 'column-class-names-2'; 34 | 35 | const allowedComponents:AllowedComponents = { 36 | applicable: true, 37 | components: [allowedComponent] 38 | }; 39 | 40 | 41 | const defaultProps:ContainerV1Properties = { 42 | id: "myid", 43 | title: "The Container", 44 | cqItems: dummyItems, 45 | cqItemsOrder: ["test", "test2"], 46 | layout: 'responsiveGrid', 47 | backgroundStyle: "background-color:red", 48 | isInEditor: false, 49 | cqPath: "/content/accordion-path", 50 | columnClassNames: { 51 | 'test': COLUMN_1_CLASS_NAMES, 52 | 'test2': COLUMN_2_CLASS_NAMES 53 | }, 54 | gridClassNames: GRID_CLASS_NAMES, 55 | allowedComponents: allowedComponents, 56 | componentMapping: ComponentMapping 57 | }; 58 | 59 | 60 | 61 | it('Renders without crashing', () => { 62 | const div = document.createElement('div'); 63 | ReactDOM.render( 64 | , 65 | div 66 | ); 67 | ReactDOM.unmountComponentAtNode(div); 68 | expect(1).toBe(1); 69 | }); 70 | 71 | 72 | let AddListenerSpy,RemoveListener,GetDataSpy: jest.SpyInstance; 73 | 74 | beforeEach(() => { 75 | AddListenerSpy = jest.spyOn(ModelManager, 'addListener'); 76 | RemoveListener = jest.spyOn(ModelManager, 'removeListener'); 77 | GetDataSpy = jest.spyOn(ModelManager, 'getData'); 78 | 79 | AddListenerSpy.mockReturnValue(); 80 | RemoveListener.mockReturnValue(); 81 | GetDataSpy.mockResolvedValue({}); 82 | 83 | }); 84 | 85 | it('Renders a responsive grid properly', () => { 86 | 87 | // const Wrapped = withComponentMappingContext(AccordionV1); 88 | const wrapper = mount(); 89 | 90 | 91 | const container = wrapper.find(".cmp-container"); 92 | 93 | 94 | //@ts-ignore 95 | const containerStyle = wrapper.find(".cmp-container").get(0).ref.current.getAttribute("style"); 96 | 97 | expect(containerStyle).toEqual("background-color:red"); 98 | 99 | const column1 = container.find("." + COLUMN_1_CLASS_NAMES); 100 | expect(column1).toHaveLength(1); 101 | expect(column1.find(".dummyCmp").text()).toEqual("Component1") 102 | 103 | const column2 = container.find("." + COLUMN_2_CLASS_NAMES); 104 | expect(column2).toHaveLength(1); 105 | expect(column2.find(".dummyCmp").text()).toEqual("Component2") 106 | 107 | 108 | 109 | }); 110 | 111 | 112 | 113 | it('Renders a basic container properly', () => { 114 | 115 | // const Wrapped = withComponentMappingContext(AccordionV1); 116 | const wrapper = mount(); 117 | 118 | const container = wrapper.find(".cmp-container"); 119 | 120 | 121 | //@ts-ignore 122 | const containerStyle = wrapper.find(".cmp-container").get(0).ref.current.getAttribute("style"); 123 | 124 | expect(containerStyle).toEqual("background-color:red"); 125 | 126 | const column1 = container.find("." + COLUMN_1_CLASS_NAMES); 127 | expect(column1).toHaveLength(0); 128 | 129 | const column2 = container.find("." + COLUMN_2_CLASS_NAMES); 130 | expect(column2).toHaveLength(0); 131 | 132 | expect(container.find(".dummyCmp")).toHaveLength(2); 133 | 134 | 135 | 136 | }); -------------------------------------------------------------------------------- /src/container/contentfragment/v1/ContentFragmentV1.test.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as React from 'react'; 18 | import { 19 | ContentFragmentV1, ContentFragmentV1Element, 20 | ContentFragmentV1Properties, 21 | DefaultContentFragmentV1Renderer, 22 | MapToContentFragmentModel 23 | } from "./"; 24 | 25 | import {mount, ReactWrapper} from "enzyme"; 26 | import ReactDOM from 'react-dom'; 27 | 28 | import * as modelJson from "./ContentFragmentV1.test.model.json"; 29 | import ComponentMapping from "../../../TestComponentMapping"; 30 | import {ContainerState, MapTo} from "@adobe/aem-react-editable-components"; 31 | import {withStandardBaseCssClass} from "../../../AbstractCoreContainerComponent"; 32 | import {ModelManager} from "@adobe/aem-spa-page-model-manager"; 33 | 34 | const defaultProps = modelJson as unknown as ContentFragmentV1Properties; 35 | 36 | MapTo("core-components-examples/wcm-react/components/content-fragment")(ContentFragmentV1); 37 | 38 | class TestModel extends DefaultContentFragmentV1Renderer { 39 | 40 | protected renderElement(element:ContentFragmentV1Element, name:string, index:number):JSX.Element{ 41 | return ( 42 |
{element.value}
43 | ) 44 | } 45 | 46 | render() { 47 | return ( 48 |
49 | 50 |

Hi! This is my custom CF component! My name is: {this.props.elements["name"].value}

51 | {this.elements} 52 | {this.childComponents} 53 | 54 |
55 | ) 56 | } 57 | } 58 | 59 | MapToContentFragmentModel("testing-frontend-react/models/custom-model")(withStandardBaseCssClass(TestModel, "cmp-custom-contentfragment")); 60 | 61 | let AddListenerSpy,RemoveListener,GetDataSpy: jest.SpyInstance; 62 | 63 | beforeEach(() => { 64 | AddListenerSpy = jest.spyOn(ModelManager, 'addListener'); 65 | RemoveListener = jest.spyOn(ModelManager, 'removeListener'); 66 | GetDataSpy = jest.spyOn(ModelManager, 'getData'); 67 | 68 | AddListenerSpy.mockReturnValue(); 69 | RemoveListener.mockReturnValue(); 70 | GetDataSpy.mockResolvedValue({}); 71 | 72 | }); 73 | 74 | it('Renders without crashing', () => { 75 | const div = document.createElement('div'); 76 | ReactDOM.render( 77 | , 78 | div 79 | ); 80 | ReactDOM.unmountComponentAtNode(div); 81 | expect(1).toBe(1); 82 | }); 83 | 84 | it('Renders everything properly.', () => { 85 | 86 | const wrapper = mount(); 87 | const contentFragment = wrapper.find('.cmp-contentfragment'); 88 | 89 | expect(contentFragment).toHaveLength(1); 90 | 91 | const subContentFragment = wrapper.find('.cmp-custom-contentfragment'); 92 | 93 | expect(subContentFragment).toHaveLength(1); 94 | 95 | }); 96 | 97 | it('Renders an empty placeholder', () => { 98 | 99 | const emptyProps:ContentFragmentV1Properties = { 100 | cqItems: {}, 101 | cqItemsOrder: [], 102 | cqPath: "", 103 | description: "", 104 | elements: {}, 105 | elementsOrder: [], 106 | id: "", 107 | isInEditor: true, 108 | model: "", 109 | title: "", 110 | componentMapping: ComponentMapping, 111 | 112 | } 113 | const wrapper = mount(); 114 | 115 | const contentFragment = wrapper.find('.cmp-contentfragment'); 116 | 117 | expect(contentFragment).toHaveLength(0); 118 | 119 | const placeholder = wrapper.find('.cf-placeholder'); 120 | 121 | expect(placeholder).toHaveLength(1); 122 | 123 | }); 124 | 125 | it('Renders no placeholder if not in author', () => { 126 | 127 | const emptyProps:ContentFragmentV1Properties = { 128 | cqItems: {}, 129 | cqItemsOrder: [], 130 | cqPath: "", 131 | description: "", 132 | elements: {}, 133 | elementsOrder: [], 134 | id: "", 135 | isInEditor: false, 136 | model: "", 137 | title: "", 138 | componentMapping: ComponentMapping, 139 | 140 | } 141 | const wrapper = mount(); 142 | 143 | const contentFragment = wrapper.find('.cmp-contentfragment'); 144 | 145 | expect(contentFragment).toHaveLength(0); 146 | 147 | const placeholder = wrapper.find('.cf-placeholder'); 148 | 149 | expect(placeholder).toHaveLength(0); 150 | 151 | }); 152 | 153 | 154 | 155 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@adobe/aem-core-components-react-spa", 3 | "version": "1.2.0", 4 | "license": "Apache-2.0", 5 | "scripts": { 6 | "clean": "shx rm -rf coverage/ dist/ src/tsconfig.types.tsbuildinfo", 7 | "linter": "eslint --ext .ts,.tsx .", 8 | "linter:fix": "eslint --ext .ts,.tsx . --fix", 9 | "test": "jest --passWithNoTests", 10 | "test-clear": "jest --clearCache", 11 | "test:coverage": "jest --coverage --passWithNoTests", 12 | "test:debug": "jest --coverage --watchAll", 13 | "build": "npm run clean && webpack --config config/webpack.config.core.js && webpack --config config/webpack.config.lib.js && tsc -p src/tsconfig.types.json", 14 | "build:production": "cross-env NODE_ENV=production npm run clean && webpack --config config/webpack.config.core.js --mode=production && webpack --config config/webpack.config.lib.js --mode=production && tsc -p src/tsconfig.types.json", 15 | "build:types": "npm run clean && tsc -p src/tsconfig.types.json", 16 | "semantic-release": "semantic-release" 17 | }, 18 | "description": "AEM Spa editor - React Implementation for the AEM Core Components", 19 | "author": { 20 | "name": "Adobe Systems Inc." 21 | }, 22 | "homepage": "https://github.com/adobe/aem-react-core-wcm-components-spa/blob/master/README.md", 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/adobe/aem-react-core-wcm-components-spa" 26 | }, 27 | "peerDependencies": { 28 | "react": "^16.14.0", 29 | "@adobe/aem-react-editable-components": "^1.1.5", 30 | "@adobe/aem-spa-component-mapping": "^1.1.1", 31 | "@adobe/aem-spa-page-model-manager": "^1.3.11" 32 | }, 33 | "devDependencies": { 34 | "@adobe/aem-react-editable-components": "^1.1.5", 35 | "@adobe/aem-spa-component-mapping": "^1.1.1", 36 | "@adobe/aem-spa-page-model-manager": "^1.3.11", 37 | "@babel/cli": "^7.10.5", 38 | "@babel/core": "^7.11.4", 39 | "@babel/plugin-proposal-class-properties": "^7.13.0", 40 | "@babel/preset-env": "^7.11.0", 41 | "@babel/preset-react": "^7.10.4", 42 | "@semantic-release/changelog": "^5.0.1", 43 | "@semantic-release/git": "^9.0.0", 44 | "@semantic-release/github": "^7.2.0", 45 | "@testing-library/jest-dom": "^4.2.4", 46 | "@testing-library/react": "^9.5.0", 47 | "@testing-library/user-event": "^7.2.1", 48 | "@types/enzyme": "^3.10.5", 49 | "@types/enzyme-adapter-react-16": "^1.0.6", 50 | "@types/jest": "^25.1.3", 51 | "@types/node": "^12.20.10", 52 | "@types/react": "16.14.6", 53 | "@types/react-dom": "^16.9.8", 54 | "@types/react-router-dom": "^5.1.5", 55 | "@typescript-eslint/eslint-plugin": "^4.23.0", 56 | "@typescript-eslint/parser": "^4.23.0", 57 | "@typescript-eslint/typescript-estree": "^4.23.0", 58 | "babel-eslint": "^10.1.0", 59 | "babel-loader": "^8.1.0", 60 | "babel-plugin-istanbul": "^5.2.0", 61 | "chai": "^4.2.0", 62 | "clean-webpack-plugin": "^3.0.0", 63 | "cross-env": "^6.0.3", 64 | "cz-conventional-changelog": "^3.3.0", 65 | "doxdox": "^3.0.0", 66 | "enzyme": "^3.11.0", 67 | "enzyme-adapter-react-16": "^1.15.3", 68 | "eslint": "^6.8.0", 69 | "eslint-plugin-react": "^7.23.2", 70 | "istanbul": "^0.4.5", 71 | "istanbul-instrumenter-loader": "^3.0.1", 72 | "jest": "26.6.3", 73 | "jest-environment-jsdom-fourteen": "1.0.1", 74 | "jest-resolve": "26.6.2", 75 | "jest-watch-typeahead": "0.6.3", 76 | "jsdoc": "^3.6.5", 77 | "markdown-include": "^0.4.3", 78 | "prop-types": "^15.7.2", 79 | "react": "^16.14.0", 80 | "react-dom": "^16.14.0", 81 | "semantic-release": "^17.4.1", 82 | "shx": "^0.3.2", 83 | "source-map-loader": "^1.1.0", 84 | "ts-jest": "^26.4.1", 85 | "ts-loader": "^8.1.0", 86 | "tslint": "^6.1.3", 87 | "tslint-etc": "^1.13.6", 88 | "tslint-no-toplevel-property-access": "^0.0.2", 89 | "tslint-no-unused-expression-chai": "0.1.4", 90 | "typescript": "^4.0.2", 91 | "webpack": "^4.46.0", 92 | "webpack-cli": "^3.3.10", 93 | "webpack-manifest-plugin": "^2.2.0", 94 | "webpack-node-externals": "^1.7.2" 95 | }, 96 | "main": "dist/index.js", 97 | "types": "dist/index.d.ts", 98 | "eslintConfig": { 99 | "extends": "plugin:@typescript-eslint/recommended", 100 | "parser": "@typescript-eslint/parser", 101 | "parserOptions": { 102 | "ecmaVersion": 2020, 103 | "sourceType": "module" 104 | }, 105 | "rules": { 106 | "@typescript-eslint/no-empty-interface": 0, 107 | "@typescript-eslint/ban-ts-comment": 0 108 | } 109 | }, 110 | "browserslist": { 111 | "production": [ 112 | ">0.2%", 113 | "not dead", 114 | "not op_mini all" 115 | ], 116 | "development": [ 117 | "last 1 chrome version", 118 | "last 1 firefox version", 119 | "last 1 safari version" 120 | ] 121 | }, 122 | "config": { 123 | "commitizen": { 124 | "path": "./node_modules/cz-conventional-changelog" 125 | } 126 | }, 127 | "release": { 128 | "plugins": [ 129 | "@semantic-release/commit-analyzer", 130 | "@semantic-release/release-notes-generator", 131 | "@semantic-release/npm", 132 | [ 133 | "@semantic-release/git", 134 | { 135 | "assets": [ 136 | "package.json" 137 | ] 138 | } 139 | ] 140 | ] 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/container/tabs/v1/TabsV1.test.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as React from 'react'; 18 | import TabsV1, {TabsV1Properties} from "./TabsV1"; 19 | import {mount, ReactWrapper} from "enzyme"; 20 | import ReactDOM from 'react-dom'; 21 | 22 | import ComponentMapping, {dummyItems} from "../../../TestComponentMapping"; 23 | import {ModelManager} from "@adobe/aem-spa-page-model-manager" 24 | import {AllowedComponent, AllowedComponents} from "@adobe/aem-react-editable-components" 25 | 26 | const allowedComponent:AllowedComponent = { 27 | path: "/apps/core/components/dummy", 28 | title: "Some Component" 29 | }; 30 | 31 | 32 | const allowedComponents:AllowedComponents = { 33 | applicable: true, 34 | components: [allowedComponent] 35 | }; 36 | 37 | 38 | const defaultProps:TabsV1Properties = { 39 | accessibilityLabel: 'Tabs', 40 | title: "The Tabs", 41 | cqItems: dummyItems, 42 | cqItemsOrder: ["test", "test2"], 43 | isInEditor: false, 44 | cqPath: "/content/tabs-path", 45 | allowedComponents: allowedComponents, 46 | componentMapping: ComponentMapping 47 | }; 48 | 49 | 50 | let AddListenerSpy,RemoveListener,GetDataSpy: jest.SpyInstance; 51 | 52 | beforeEach(() => { 53 | AddListenerSpy = jest.spyOn(ModelManager, 'addListener'); 54 | RemoveListener = jest.spyOn(ModelManager, 'removeListener'); 55 | GetDataSpy = jest.spyOn(ModelManager, 'getData'); 56 | 57 | AddListenerSpy.mockReturnValue(); 58 | RemoveListener.mockReturnValue(); 59 | GetDataSpy.mockResolvedValue({}); 60 | 61 | }); 62 | 63 | it('Renders without crashing', () => { 64 | const div = document.createElement('div'); 65 | ReactDOM.render( 66 | , 67 | div 68 | ); 69 | ReactDOM.unmountComponentAtNode(div); 70 | expect(1).toBe(1); 71 | }); 72 | 73 | const validateComponentPresent = (wrapper:ReactWrapper, text:string) =>{ 74 | 75 | wrapper.update(); 76 | const dummyComp = wrapper.find(".dummyCmp"); 77 | expect(dummyComp).toHaveLength(1); 78 | expect(dummyComp.text()).toEqual(text); 79 | }; 80 | 81 | 82 | it('Renders tabs properly and changes on click events', () => { 83 | 84 | // const Wrapped = withComponentMappingContext(AccordionV1); 85 | const wrapper = mount(); 86 | 87 | 88 | const container = wrapper.find(".cmp-tabs"); 89 | 90 | expect(container).toHaveLength(1); 91 | 92 | validateComponentPresent(wrapper, "Component1"); 93 | 94 | const anchors = wrapper.find(".cmp-tabs__tab"); 95 | 96 | expect(anchors).toHaveLength(2); 97 | 98 | const tab1 = anchors.first(); 99 | const tab2 = anchors.last(); 100 | 101 | tab2.simulate("click"); 102 | 103 | validateComponentPresent(wrapper, "Component2"); 104 | 105 | tab1.simulate("click"); 106 | 107 | validateComponentPresent(wrapper, "Component1"); 108 | 109 | 110 | }); 111 | 112 | it('Changes when you switch tabs in author mode', () => { 113 | 114 | const wrapper = mount(); 115 | const accordionRoot = wrapper.find('.cmp-tabs'); 116 | 117 | expect(accordionRoot).toHaveLength(1); 118 | 119 | //@ts-ignore 120 | window.Granite.author.trigger("/content/tabs-path", 1); 121 | 122 | validateComponentPresent(wrapper,"Component2"); 123 | //@ts-ignore 124 | window.Granite.author.trigger("/content/tabs-path", 0); 125 | validateComponentPresent(wrapper,"Component1"); 126 | }); 127 | 128 | 129 | const validateAuthorTabVisible = (wrapper:ReactWrapper, component : 'Component1' | 'Component2') =>{ 130 | 131 | wrapper.update(); 132 | 133 | const tabContents = wrapper.find(".cmp-tabs__author-tab-content"); 134 | 135 | expect(tabContents).toHaveLength(2); 136 | 137 | const tabContent1 = tabContents.first(); 138 | const tabContent2 = tabContents.last(); 139 | 140 | if(component === 'Component1'){ 141 | expect(tabContent1.prop("style")).toHaveProperty("display", "block"); 142 | expect(tabContent2.prop("style")).toHaveProperty("display", "none"); 143 | }else{ 144 | expect(tabContent1.prop("style")).toHaveProperty("display", "none"); 145 | expect(tabContent2.prop("style")).toHaveProperty("display", "block"); 146 | } 147 | }; 148 | 149 | it('Renders everything in author mode with display none on hidden tabs ', () => { 150 | 151 | // const Wrapped = withComponentMappingContext(AccordionV1); 152 | const wrapper = mount(); 153 | 154 | 155 | const container = wrapper.find(".cmp-tabs"); 156 | 157 | expect(container).toHaveLength(1); 158 | 159 | const anchors = wrapper.find(".cmp-tabs__tab"); 160 | 161 | expect(anchors).toHaveLength(2); 162 | 163 | const tab1 = anchors.first(); 164 | const tab2 = anchors.last(); 165 | 166 | validateAuthorTabVisible(wrapper, 'Component1'); 167 | 168 | tab2.simulate("click"); 169 | 170 | validateAuthorTabVisible(wrapper, 'Component2'); 171 | 172 | tab1.simulate("click"); 173 | 174 | validateAuthorTabVisible(wrapper, 'Component1'); 175 | 176 | 177 | }); 178 | 179 | -------------------------------------------------------------------------------- /src/container/tabs/v1/TabsV1.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import React from "react"; 18 | import {ComponentMapping, Container} from '@adobe/aem-react-editable-components'; 19 | import {CoreContainerProperties, CoreContainerState, withAuthorPanelSwitch, withStandardBaseCssClass, CoreContainerItem} from "../../../AbstractCoreContainerComponent"; 20 | import {TabsV1IsEmptyFn} from "./TabsV1IsEmptyFn"; 21 | 22 | export interface TabsV1Properties extends CoreContainerProperties{ 23 | 24 | accessibilityLabel:string; 25 | activeItem?: string; 26 | cqItems: { [key: string]: CoreContainerItem }; 27 | } 28 | 29 | 30 | export interface TabsV1State extends CoreContainerState{ 31 | activeIndex: number; 32 | } 33 | 34 | 35 | class TabsV1Impl extends Container { 36 | 37 | constructor(props:TabsV1Properties) { 38 | super(props); 39 | 40 | this.state = { 41 | activeIndex: (!!props.activeItem && props.activeItem.length > 0) ? this.props.cqItemsOrder.indexOf(props.activeItem) : 0, 42 | componentMapping: this.props.componentMapping || ComponentMapping 43 | }; 44 | 45 | this.handleTabNavClick = this.handleTabNavClick.bind(this); 46 | } 47 | 48 | componentDidUpdate(prevProps: Readonly, prevState: Readonly, snapshot?: any): void { 49 | if(this.props.activeIndexFromAuthorPanel !== undefined && prevProps.activeIndexFromAuthorPanel != this.props.activeIndexFromAuthorPanel){ 50 | this.setState({ activeIndex: this.props.activeIndexFromAuthorPanel } ); 51 | } 52 | } 53 | 54 | /** 55 | * Overload childComponents getter to only return the active tab's items. 56 | * @returns {Object[]} An array with the components instantiated to JSX 57 | */ 58 | tabbedChildComponents() { 59 | 60 | if(this.props.isInEditor === true){ 61 | //for editing capabilities to work properly, we always need to render each item. 62 | //we will hide the disabled items instead. 63 | return ( 64 | <> 65 | { 66 | this.childComponents.map((item, index) => { 67 | const isVisible = (this.state.activeIndex === index); 68 | const styles = { display: (!isVisible) ? 'none' : 'block'}; 69 | return ( 70 |
{this.childComponents[index]}
71 | ) 72 | }) 73 | } 74 | 75 | ) 76 | }else{ 77 | //when the editor is disabled, we can just show the active item only. 78 | return this.childComponents[this.state.activeIndex]; 79 | } 80 | } 81 | 82 | handleTabNavClick(index:number){ 83 | if(this.state.activeIndex !== index){ 84 | this.setState({ 85 | activeIndex: index 86 | }); 87 | } 88 | } 89 | 90 | tabNavigation(){ 91 | 92 | const childComponents:JSX.Element[] = []; 93 | 94 | if (!this.props.cqItems || !this.props.cqItemsOrder) { 95 | return childComponents; 96 | } 97 | 98 | return ( 99 |
    103 | { 104 | this.props.cqItemsOrder.map((item, index) => { 105 | const tab = this.props.cqItems[item]; 106 | const isActive = (index === this.state.activeIndex); 107 | return ( 108 |
  1. this.handleTabNavClick(index)} 111 | className={this.props.baseCssClass + '__tab' + (isActive ? ' ' + this.props.baseCssClass + '__tab--active' : '')} 112 | tabIndex={isActive ? 0 : -1} 113 | data-cmp-hook-tabs="tab"> 114 | {tab['cq:panelTitle']} 115 |
  2. 116 | ) 117 | 118 | }) 119 | } 120 | 121 |
122 | ) 123 | 124 | } 125 | 126 | get tabContainerProps(){ 127 | const attrs = this.containerProps; 128 | attrs['className'] = attrs.className + ' ' + this.props.baseCssClass; 129 | attrs['data-cmp-is'] = 'tabs'; 130 | attrs['data-panelcontainer'] = 'tabs'; 131 | return attrs; 132 | } 133 | 134 | render() { 135 | 136 | const isEmpty = TabsV1IsEmptyFn(this.props); 137 | 138 | return ( 139 |
140 | { !isEmpty && this.tabNavigation() } 141 | { !isEmpty && this.tabbedChildComponents() } 142 | { this.placeholderComponent } 143 |
144 | ) 145 | } 146 | 147 | } 148 | 149 | export default withStandardBaseCssClass(withAuthorPanelSwitch(TabsV1Impl), "cmp-tabs"); -------------------------------------------------------------------------------- /src/container/accordion/v1/AccordionV1.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as React from 'react'; 18 | 19 | 20 | import {CoreContainerProperties, CoreContainerState, withAuthorPanelSwitch, withStandardBaseCssClass} from "../../../AbstractCoreContainerComponent"; 21 | import {ComponentMapping, Container} from '@adobe/aem-react-editable-components'; 22 | import {AccordionV1IsEmptyFn} from "./AccordionV1IsEmptyFn"; 23 | 24 | export interface AccordionV1Properties extends CoreContainerProperties{ 25 | singleExpansion: boolean; 26 | headingElement: string; 27 | expandedItems: string[]; 28 | } 29 | 30 | export interface AccordionV1State extends CoreContainerState{ 31 | expandedItems: string[]; 32 | } 33 | 34 | class AccordionV1Impl extends Container { 35 | 36 | constructor(props:AccordionV1Properties) { 37 | super(props); 38 | 39 | this.state = { 40 | componentMapping: this.props.componentMapping || ComponentMapping, 41 | expandedItems: this.props.expandedItems 42 | }; 43 | 44 | this.handleAccordionNavClick = this.handleAccordionNavClick.bind(this); 45 | } 46 | 47 | componentDidUpdate(prevProps: Readonly, prevState: Readonly): void { 48 | if(this.props.activeIndexFromAuthorPanel !== undefined && prevProps.activeIndexFromAuthorPanel != this.props.activeIndexFromAuthorPanel){ 49 | this.setState({ expandedItems: [this.props.cqItemsOrder[this.props.activeIndexFromAuthorPanel]] } ); 50 | } 51 | } 52 | 53 | handleAccordionNavClick(itemKey:string){ 54 | 55 | const isActive = this.state.expandedItems.indexOf(itemKey) > -1; 56 | const isSingleExpansion = this.props.singleExpansion; 57 | 58 | let expandedItems = this.state.expandedItems; 59 | if(isSingleExpansion){ 60 | expandedItems = (isActive) ? [] : [itemKey]; 61 | }else{ 62 | if(isActive){ 63 | const index = this.state.expandedItems.indexOf(itemKey); 64 | expandedItems.splice( index ); 65 | }else{ 66 | expandedItems.push(itemKey); 67 | } 68 | } 69 | this.setState({ 70 | expandedItems: expandedItems 71 | }); 72 | } 73 | 74 | isItemExpanded(key:string){ 75 | return this.state.expandedItems.indexOf(key) > -1; 76 | } 77 | 78 | get accordionContainerProps(){ 79 | const attrs = this.containerProps; 80 | attrs['className'] = attrs.className + ' ' + this.props.baseCssClass; 81 | attrs['data-cmp-is'] = 'accordion'; 82 | attrs['data-panelcontainer'] = 'accordion'; 83 | return attrs; 84 | } 85 | 86 | 87 | displayItem(key:string,isExpanded:boolean) { 88 | 89 | const indexToShow = this.props.cqItemsOrder.indexOf(key); 90 | 91 | if(this.props.isInEditor === true || isExpanded){ 92 | const cssClass = isExpanded ? `${this.props.baseCssClass}__panel ${this.props.baseCssClass}__panel--expanded`: `${this.props.baseCssClass}__panel ${this.props.baseCssClass}__panel--hidden`; 93 | 94 | return ( 95 |
97 | {this.childComponents[indexToShow]} 98 |
99 | ) 100 | } 101 | 102 | return null; 103 | } 104 | 105 | renderHeadingButton(key:string, item:any,buttonCssClass:string){ 106 | return ( 107 | 111 | ) 112 | } 113 | 114 | get accordionContent(){ 115 | return ( 116 | 117 | this.props.cqItemsOrder.map((key, index) => { 118 | const item = this.props.cqItems[key]; 119 | const isExpanded = this.isItemExpanded(key); 120 | const buttonCssClass = (isExpanded) ? `${this.props.baseCssClass}__button ${this.props.baseCssClass}__button--expanded` : `${this.props.baseCssClass}__button`; 121 | return ( 122 |
126 | { 127 | React.createElement( 128 | `${this.props.headingElement || 'h3'}`, 129 | { 130 | className: this.props.baseCssClass + '__header', 131 | }, 132 | this.renderHeadingButton(key,item,buttonCssClass) 133 | ) 134 | } 135 | {this.displayItem(key, isExpanded)} 136 | 137 |
138 | ) 139 | }) 140 | ); 141 | } 142 | 143 | render() { 144 | 145 | const isEmpty = AccordionV1IsEmptyFn(this.props); 146 | 147 | return ( 148 |
149 | { !isEmpty && this.accordionContent } 150 | { this.placeholderComponent } 151 |
152 | ) 153 | } 154 | 155 | } 156 | 157 | export default withStandardBaseCssClass(withAuthorPanelSwitch(AccordionV1Impl), "cmp-accordion"); -------------------------------------------------------------------------------- /src/container/accordion/v1/AccordionV1.test.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as React from 'react'; 18 | import AccordionV1, {AccordionV1Properties} from "./AccordionV1"; 19 | import {mount, ReactWrapper} from "enzyme"; 20 | import ReactDOM from 'react-dom'; 21 | 22 | import ComponentMapping, {dummyItems} from "../../../TestComponentMapping"; 23 | import {ModelManager} from "@adobe/aem-spa-page-model-manager" 24 | import {AllowedComponents, AllowedComponent, withModel} from "@adobe/aem-react-editable-components" 25 | 26 | const allowedComponent:AllowedComponent = { 27 | path: "/apps/core/components/dummy", 28 | title: "Some Component" 29 | }; 30 | 31 | const allowedComponents:AllowedComponents = { 32 | applicable: true, 33 | components: [allowedComponent] 34 | }; 35 | const defaultProps:AccordionV1Properties = { 36 | expandedItems: ["test"], 37 | headingElement: "h2", 38 | singleExpansion: false, 39 | cqItems: dummyItems, 40 | cqItemsOrder: ["test", "test2"], 41 | title: "Accordion", 42 | isInEditor: false, 43 | cqPath: "/content/accordion-path", 44 | allowedComponents: allowedComponents, 45 | componentMapping: ComponentMapping 46 | }; 47 | 48 | 49 | it('Renders without crashing', () => { 50 | const div = document.createElement('div'); 51 | ReactDOM.render( 52 | , 53 | div 54 | ); 55 | ReactDOM.unmountComponentAtNode(div); 56 | expect(1).toBe(1); 57 | }); 58 | 59 | 60 | let AddListenerSpy,RemoveListener,GetDataSpy: jest.SpyInstance; 61 | 62 | const validateComponentPresent = (wrapper:ReactWrapper, text:string) =>{ 63 | wrapper.update(); 64 | const dummyComp = wrapper.find(".dummyCmp"); 65 | expect(dummyComp).toHaveLength(1); 66 | expect(dummyComp.text()).toEqual(text); 67 | }; 68 | 69 | 70 | beforeEach(() => { 71 | AddListenerSpy = jest.spyOn(ModelManager, 'addListener'); 72 | RemoveListener = jest.spyOn(ModelManager, 'removeListener'); 73 | GetDataSpy = jest.spyOn(ModelManager, 'getData'); 74 | 75 | AddListenerSpy.mockReturnValue(); 76 | RemoveListener.mockReturnValue(); 77 | GetDataSpy.mockResolvedValue({}); 78 | 79 | }); 80 | 81 | it('Renders a basic accordion properly', () => { 82 | 83 | const wrapper = mount(); 84 | const accordionRoot = wrapper.find('.cmp-accordion'); 85 | 86 | expect(accordionRoot).toHaveLength(1); 87 | 88 | validateComponentPresent(wrapper,"Component1"); 89 | 90 | }); 91 | 92 | it('Renders out all items in author mode with hidden CSS', () => { 93 | 94 | const wrapper = mount(); 95 | const accordionRoot = wrapper.find('.cmp-accordion'); 96 | 97 | expect(accordionRoot).toHaveLength(1); 98 | 99 | const component = accordionRoot.find(".dummyCmp"); 100 | expect(component).toHaveLength(2); 101 | 102 | }); 103 | 104 | it('Changes item when you click - single expansion', () => { 105 | 106 | // const Wrapped = withComponentMappingContext(AccordionV1); 107 | const wrapper = mount(); 108 | const accordionRoot = wrapper.find('.cmp-accordion'); 109 | 110 | expect(accordionRoot).toHaveLength(1); 111 | 112 | validateComponentPresent(wrapper,"Component1"); 113 | 114 | const button2 = wrapper.find(".cmp-accordion__item:last-child .cmp-accordion__button"); 115 | 116 | button2.simulate("click"); 117 | 118 | wrapper.update(); 119 | button2.update(); 120 | 121 | expect(button2.html()).toEqual(""); 122 | 123 | validateComponentPresent(wrapper,"Component2"); 124 | 125 | 126 | }); 127 | 128 | it('Changes when you switch tab in author mode', () => { 129 | 130 | const wrapper = mount(); 131 | const accordionRoot = wrapper.find('.cmp-accordion'); 132 | 133 | expect(accordionRoot).toHaveLength(1); 134 | 135 | //@ts-ignore 136 | window.Granite.author.trigger("/content/accordion-path", 1); 137 | 138 | validateComponentPresent(wrapper,"Component2"); 139 | //@ts-ignore 140 | window.Granite.author.trigger("/content/accordion-path", 0); 141 | validateComponentPresent(wrapper,"Component1"); 142 | }); 143 | 144 | it('Changes item when you click - multi expansion', () => { 145 | 146 | // const Wrapped = withComponentMappingContext(AccordionV1); 147 | const wrapper = mount(); 148 | const accordionRoot = wrapper.find('.cmp-accordion'); 149 | 150 | expect(accordionRoot).toHaveLength(1); 151 | 152 | validateComponentPresent(wrapper,"Component1"); 153 | 154 | 155 | const headingElement = wrapper.find(".cmp-accordion__item:last-child .cmp-accordion__header"); 156 | expect(headingElement.is("h2")).toEqual(true); 157 | 158 | const button2 = wrapper.find(".cmp-accordion__item:last-child .cmp-accordion__button"); 159 | 160 | button2.simulate("click"); 161 | 162 | wrapper.update(); 163 | button2.update(); 164 | 165 | expect(button2.html()).toEqual(""); 166 | 167 | const component = wrapper.find(".dummyCmp"); 168 | 169 | expect(component).toHaveLength(2); 170 | 171 | }); -------------------------------------------------------------------------------- /src/container/carousel/v1/CarouselV1.test.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as React from 'react'; 18 | import CarouselV1 , {CarouselV1Properties} from "./CarouselV1"; 19 | import {mount, ReactWrapper} from "enzyme"; 20 | import ReactDOM from 'react-dom'; 21 | 22 | import ComponentMapping, {dummyItems} from "../../../TestComponentMapping"; 23 | import {ModelManager} from "@adobe/aem-spa-page-model-manager" 24 | import {AllowedComponents, AllowedComponent } from "@adobe/aem-react-editable-components" 25 | 26 | const allowedComponent:AllowedComponent = { 27 | path: "/apps/core/components/dummy", 28 | title: "Some Component" 29 | }; 30 | 31 | 32 | const allowedComponents:AllowedComponents = { 33 | applicable: true, 34 | components: [allowedComponent] 35 | }; 36 | const defaultProps:CarouselV1Properties = { 37 | accessibilityLabel: 'Carousel', 38 | accessibility: { 39 | play: 'Play', 40 | pause: 'Pause', 41 | next: 'Next', 42 | previous: 'Previous', 43 | slide: 'Slide {0} of {1}', 44 | indicator: 'Slide %{0}', 45 | indicators: 'Choose a slide to display' 46 | }, 47 | autopauseDisabled: false, 48 | autoplay: true, 49 | delay: 100, 50 | cqItems: dummyItems, 51 | cqItemsOrder: ["test", "test2"], 52 | title: "Accordion", 53 | isInEditor: false, 54 | cqPath: "/content/carousel-path", 55 | allowedComponents: allowedComponents, 56 | componentMapping: ComponentMapping 57 | }; 58 | 59 | 60 | it('Renders without crashing', () => { 61 | const div = document.createElement('div'); 62 | ReactDOM.render( 63 | , 64 | div 65 | ); 66 | ReactDOM.unmountComponentAtNode(div); 67 | expect(1).toBe(1); 68 | }); 69 | 70 | 71 | let AddListenerSpy,RemoveListener,GetDataSpy: jest.SpyInstance; 72 | 73 | beforeEach(() => { 74 | AddListenerSpy = jest.spyOn(ModelManager, 'addListener'); 75 | RemoveListener = jest.spyOn(ModelManager, 'removeListener'); 76 | GetDataSpy = jest.spyOn(ModelManager, 'getData'); 77 | 78 | AddListenerSpy.mockReturnValue(); 79 | RemoveListener.mockReturnValue(); 80 | GetDataSpy.mockResolvedValue({}); 81 | 82 | }); 83 | 84 | const validateComponentPresent = (wrapper:ReactWrapper, text:string) =>{ 85 | 86 | wrapper.update(); 87 | const dummyComp = wrapper.find(".dummyCmp"); 88 | expect(dummyComp).toHaveLength(1); 89 | expect(dummyComp.text()).toEqual(text); 90 | }; 91 | 92 | 93 | it('Renders a basic carousel properly and reacts on clicks', () => { 94 | 95 | // const Wrapped = withComponentMappingContext(AccordionV1); 96 | const wrapper = mount(); 97 | const content = wrapper.find('.cmp-carousel__content'); 98 | 99 | expect(content).toHaveLength(1); 100 | 101 | validateComponentPresent(wrapper, "Component1"); 102 | 103 | expect(wrapper.find(".cmp-carousel__item--active").text()).toEqual("Component1"); 104 | expect(wrapper.find(".cmp-carousel__item").first().prop("aria-label")).toEqual("Slide 1 of 2"); 105 | expect(wrapper.find(".cmp-carousel__item").last().prop("aria-label")).toEqual("Slide 2 of 2"); 106 | 107 | expect(wrapper.find(".cmp-carousel__action")).toHaveLength(4); 108 | 109 | const pauseButton = wrapper.find(".cmp-carousel__action--pause"); 110 | expect(pauseButton.prop("aria-label")).toEqual("Pause"); 111 | 112 | const playButton = wrapper.find(".cmp-carousel__action--play"); 113 | expect(playButton.prop("aria-label")).toEqual("Play"); 114 | 115 | const prevButton = wrapper.find(".cmp-carousel__action--previous"); 116 | const nextButton = wrapper.find(".cmp-carousel__action--next"); 117 | expect(nextButton).toHaveLength(1); 118 | nextButton.simulate('click'); 119 | 120 | validateComponentPresent(wrapper, "Component2"); 121 | 122 | prevButton.simulate("click"); 123 | 124 | validateComponentPresent(wrapper, "Component1"); 125 | 126 | const indicator1 = wrapper.find(".cmp-carousel__indicators li").first(); 127 | const indicator2 = wrapper.find(".cmp-carousel__indicators li").last(); 128 | 129 | indicator2.simulate('click'); 130 | 131 | validateComponentPresent(wrapper, "Component2"); 132 | 133 | indicator1.simulate('click'); 134 | 135 | validateComponentPresent(wrapper, "Component1"); 136 | 137 | }); 138 | 139 | 140 | it('Changes when you switch slide in author mode', () => { 141 | 142 | const wrapper = mount(); 143 | const accordionRoot = wrapper.find('.cmp-carousel'); 144 | 145 | expect(accordionRoot).toHaveLength(1); 146 | 147 | //@ts-ignore 148 | window.Granite.author.trigger("/content/carousel-path", 1); 149 | 150 | validateComponentPresent(wrapper,"Component2"); 151 | //@ts-ignore 152 | window.Granite.author.trigger("/content/carousel-path", 0); 153 | validateComponentPresent(wrapper,"Component1"); 154 | }); 155 | 156 | 157 | it('Automatically slides forward', () => { 158 | 159 | // const Wrapped = withComponentMappingContext(AccordionV1); 160 | const wrapper = mount(); 161 | const content = wrapper.find('.cmp-carousel__content'); 162 | 163 | expect(content).toHaveLength(1); 164 | validateComponentPresent(wrapper, "Component1"); 165 | 166 | jest.advanceTimersByTime(150); 167 | 168 | validateComponentPresent(wrapper, "Component2"); 169 | 170 | }); 171 | 172 | it('Does NOT Automatically slide forward if we turn it off', () => { 173 | 174 | // const Wrapped = withComponentMappingContext(AccordionV1); 175 | const wrapper = mount(); 176 | const content = wrapper.find('.cmp-carousel__content'); 177 | 178 | expect(content).toHaveLength(1); 179 | 180 | const pauseButton = wrapper.find(".cmp-carousel__action--pause"); 181 | 182 | expect(pauseButton).toHaveLength(0); 183 | 184 | jest.advanceTimersByTime(150); 185 | 186 | validateComponentPresent(wrapper, "Component1"); 187 | 188 | }); 189 | 190 | it('Does NOT Automatically slide forward if we click pause, and resumes if we click resume', () => { 191 | 192 | // const Wrapped = withComponentMappingContext(AccordionV1); 193 | const wrapper = mount(); 194 | const content = wrapper.find('.cmp-carousel__content'); 195 | 196 | expect(content).toHaveLength(1); 197 | 198 | const pauseButton = wrapper.find(".cmp-carousel__action--pause"); 199 | 200 | pauseButton.simulate("click"); 201 | 202 | validateComponentPresent(wrapper, "Component1"); 203 | 204 | jest.advanceTimersByTime(150); 205 | 206 | validateComponentPresent(wrapper, "Component1"); 207 | 208 | const resumeButton = wrapper.find(".cmp-carousel__action--play"); 209 | 210 | resumeButton.simulate("click"); 211 | jest.advanceTimersByTime(150); 212 | resumeButton.simulate("click"); 213 | 214 | }); 215 | 216 | 217 | it('Temporary stops sliding if we hover over it, and resume once we hover out.', () => { 218 | 219 | // const Wrapped = withComponentMappingContext(AccordionV1); 220 | const wrapper = mount(); 221 | const content = wrapper.find('.cmp-carousel__content'); 222 | 223 | expect(content).toHaveLength(1); 224 | 225 | //trigger hover in 226 | content.simulate('mouseenter'); 227 | 228 | jest.advanceTimersByTime(150); 229 | 230 | validateComponentPresent(wrapper, "Component1"); 231 | 232 | //trigger hover out 233 | content.simulate('mouseleave'); 234 | jest.advanceTimersByTime(150); 235 | 236 | validateComponentPresent(wrapper, "Component2"); 237 | 238 | 239 | }); 240 | 241 | 242 | it('Does not temporarily stop sliding if I hover over it, if we disabled autopause', () => { 243 | 244 | // const Wrapped = withComponentMappingContext(AccordionV1); 245 | const wrapper = mount(); 246 | const content = wrapper.find('.cmp-carousel__content'); 247 | 248 | expect(content).toHaveLength(1); 249 | 250 | //trigger hover in 251 | content.simulate('mouseenter'); 252 | 253 | validateComponentPresent(wrapper, "Component1"); 254 | 255 | jest.advanceTimersByTime(150); 256 | 257 | validateComponentPresent(wrapper, "Component2"); 258 | 259 | }); 260 | 261 | 262 | it('Renders a basic carousel without autoplay', () => { 263 | 264 | // const Wrapped = withComponentMappingContext(AccordionV1); 265 | const wrapper = mount(); 266 | const content = wrapper.find('.cmp-carousel__content'); 267 | 268 | expect(content).toHaveLength(1); 269 | 270 | validateComponentPresent(wrapper, "Component1"); 271 | 272 | expect(wrapper.find(".cmp-carousel__item--active").text()).toEqual("Component1"); 273 | expect(wrapper.find(".cmp-carousel__item").first().prop("aria-label")).toEqual("Slide 1 of 2"); 274 | expect(wrapper.find(".cmp-carousel__item").last().prop("aria-label")).toEqual("Slide 2 of 2"); 275 | 276 | expect(wrapper.find(".cmp-carousel__action")).toHaveLength(2); 277 | 278 | 279 | }); 280 | 281 | it('Renders out all slides in author mode with hidden CSS', () => { 282 | 283 | // const Wrapped = withComponentMappingContext(AccordionV1); 284 | const wrapper = mount(); 285 | const content = wrapper.find('.cmp-carousel__content'); 286 | 287 | expect(content).toHaveLength(1); 288 | 289 | const dummyComp = wrapper.find(".dummyCmp"); 290 | 291 | expect(dummyComp).toHaveLength(2); 292 | 293 | }); 294 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/container/carousel/v1/CarouselV1.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import React from 'react'; 18 | 19 | 20 | import {CoreContainerProperties, CoreContainerState, withAuthorPanelSwitch, withStandardBaseCssClass, CoreContainerItem} from "../../../AbstractCoreContainerComponent"; 21 | import {ComponentMapping, Container} from '@adobe/aem-react-editable-components'; 22 | 23 | import {CarouselV1IsEmptyFn} from "./CarouselV1IsEmptyFn"; 24 | import {TabsV1Properties, TabsV1State} from "../../.."; 25 | 26 | 27 | const formatFn = (value:string, args:string[]) => { 28 | let content = value; 29 | for (let i = 0; i < args.length; i++) { 30 | const replacement = '{' + i + '}'; 31 | content = content.replace(replacement, args[i]); 32 | } 33 | return content; 34 | }; 35 | 36 | 37 | 38 | export interface CarouselV1Properties extends CoreContainerProperties{ 39 | autoplay: boolean; 40 | autopauseDisabled: boolean; 41 | accessibilityLabel:string; 42 | accessibility: CarouselV1AccessibilityProperties; 43 | delay: number; 44 | cqItems: { [key: string]: CoreContainerItem }; 45 | } 46 | 47 | export interface CarouselV1AccessibilityProperties{ 48 | play: string; 49 | pause: string; 50 | next: string; 51 | previous: string; 52 | slide: string; 53 | indicator: string; 54 | indicators: string; 55 | } 56 | export interface CarouselV1State extends CoreContainerState{ 57 | activeIndex: number, 58 | isMouseEntered: boolean, 59 | autoPlay: boolean, 60 | } 61 | 62 | class CarouselV1Impl extends Container { 63 | 64 | interval = 0; 65 | 66 | 67 | static defaultProps = { 68 | _allowedComponentPlaceholderListEmptyLabel: 'CarouselV1', 69 | isInEditor: false, 70 | autoplay: false, 71 | cqPath: '', 72 | cqItems: {}, 73 | cqItemsOrder: [], 74 | accessibilityLabel: 'Carousel', 75 | accessibility: { 76 | play: 'Play', 77 | pause: 'Pause', 78 | next: 'Next', 79 | previous: 'Previous', 80 | slide: 'Slide {0} of {1}', 81 | indicator: 'Slide %{0}', 82 | indicators: 'Choose a slide to display' 83 | } 84 | }; 85 | 86 | constructor(props:CarouselV1Properties) { 87 | super(props); 88 | 89 | //@ts-ignore 90 | this.state = { 91 | activeIndex: 0, 92 | isMouseEntered: false, 93 | autoPlay: this.props.autoplay && !this.props.isInEditor, 94 | componentMapping: this.props.componentMapping || ComponentMapping 95 | }; 96 | 97 | this.handleIndicatorClick = this.handleIndicatorClick.bind(this); 98 | this.handleOnButtonPrev = this.handleOnButtonPrev.bind(this); 99 | this.handleOnButtonNext = this.handleOnButtonNext.bind(this); 100 | this.handleOnMouseEnter = this.handleOnMouseEnter.bind(this); 101 | this.handleOnMouseLeave = this.handleOnMouseLeave.bind(this); 102 | 103 | } 104 | 105 | componentDidUpdate(prevProps: Readonly, prevState: Readonly, snapshot?: any): void { 106 | if(this.props.activeIndexFromAuthorPanel !== undefined && prevProps.activeIndexFromAuthorPanel != this.props.activeIndexFromAuthorPanel){ 107 | this.setState({ activeIndex: this.props.activeIndexFromAuthorPanel } ); 108 | this.toggleAutoPlay(false); 109 | } 110 | } 111 | 112 | componentDidMount(){ 113 | this.autoPlay(); 114 | } 115 | 116 | componentWillUnmount(){ 117 | if(this.interval){ 118 | this.clearAutoPlay(); 119 | } 120 | } 121 | 122 | handleOnMouseEnter(){ 123 | if(!this.props.autopauseDisabled && this.props.autoplay){ 124 | this.clearAutoPlay(); 125 | } 126 | } 127 | 128 | handleOnMouseLeave(){ 129 | if(!this.props.autopauseDisabled && this.props.autoplay){ 130 | this.autoPlay(); 131 | } 132 | } 133 | 134 | handleOnButtonPrev(){ 135 | this.prevSlide(); 136 | } 137 | 138 | handleOnButtonNext(){ 139 | this.nextSlide(); 140 | } 141 | 142 | 143 | handleIndicatorClick(index:number){ 144 | 145 | if(this.state.activeIndex !== index){ 146 | 147 | this.setState({ 148 | activeIndex: index 149 | }); 150 | } 151 | } 152 | 153 | autoPlay(){ 154 | this.interval = window.setInterval(() => { 155 | this.autoPlayTick(); 156 | }, this.props.delay); 157 | } 158 | 159 | autoPlayTick() { 160 | 161 | if (!this.state.autoPlay || this.props.cqItemsOrder.length <= 1) { 162 | return; 163 | } 164 | 165 | this.nextSlide(); 166 | } 167 | 168 | clearAutoPlay = () => { 169 | window.clearInterval(this.interval); 170 | }; 171 | 172 | toggleAutoPlay(toggle:boolean){ 173 | this.setState({ 174 | autoPlay: toggle 175 | }) 176 | } 177 | 178 | nextSlide(){ 179 | 180 | const activeIndex = this.__getActiveIndex(); 181 | 182 | if(activeIndex=== (this.props.cqItemsOrder.length-1)){ 183 | 184 | this.__setSlide(0); 185 | }else{ 186 | this.__setSlide(activeIndex + 1); 187 | } 188 | } 189 | 190 | prevSlide(){ 191 | const activeIndex = this.__getActiveIndex(); 192 | if(activeIndex === 0){ 193 | 194 | this.__setSlide(this.props.cqItemsOrder.length - 1); 195 | }else{ 196 | this.__setSlide(activeIndex - 1); 197 | } 198 | } 199 | 200 | __getActiveIndex(){ 201 | 202 | return this.state.activeIndex; 203 | } 204 | 205 | __setSlide(index:number){ 206 | 207 | this.setState({ 208 | activeIndex: index 209 | }); 210 | } 211 | 212 | 213 | 214 | render() { 215 | 216 | const isEmpty = CarouselV1IsEmptyFn(this.props); 217 | return ( 218 |
223 | { 224 | !isEmpty && this.renderCarousel() 225 | } 226 | { this.placeholderComponent } 227 |
228 | ) 229 | 230 | } 231 | 232 | displayItem(item:JSX.Element, index:number){ 233 | 234 | const isActive = index === this.state.activeIndex; 235 | //we display the item if active is true, or if we are in the author mode. we need to always display the item for the author mode to work properly. 236 | const display = !!(isActive || this.props.isInEditor); 237 | 238 | const cssClass = isActive ? `${this.props.baseCssClass}__item ${this.props.baseCssClass}__item--active` : `${this.props.baseCssClass}__item`; 239 | const ariaLabel = formatFn(this.props.accessibility.slide, [(index + 1).toString(), this.props.cqItemsOrder.length.toString()]); 240 | 241 | return ( 242 |
247 | {display && item} 248 |
249 | ) 250 | } 251 | 252 | renderCarousel(){ 253 | return ( 254 | 255 |
this.handleOnMouseEnter()} onMouseLeave={()=>this.handleOnMouseLeave()} > 256 | { 257 | this.childComponents.map((childComponent, index) => this.displayItem(childComponent,index)) 258 | } 259 | {this.renderCarouselActions()} 260 | {this.renderCarouselIndicators()} 261 |
262 | ) 263 | } 264 | 265 | renderCarouselIndicators(){ 266 | return ( 267 |
    270 | { 271 | 272 | this.props.cqItemsOrder.map((key, index) => { 273 | 274 | const item:CoreContainerItem = this.props.cqItems[key]; 275 | 276 | const cssClass = (index === this.state.activeIndex) ? `${this.props.baseCssClass}__indicator ${this.props.baseCssClass}__indicator--active` : `${this.props.baseCssClass}__indicator`; 277 | const ariaLabelItem = formatFn(this.props.accessibility.indicator, [(index + 1).toString()]); 278 | return ( 279 |
  1. this.handleIndicatorClick(index)} 282 | className={cssClass} 283 | role="tab" 284 | aria-label={ariaLabelItem}>{item["cq:panelTitle"]}
  2. 285 | ) 286 | }) 287 | } 288 | 289 |
290 | ); 291 | } 292 | renderCarouselActions(){ 293 | return ( 294 |
295 | 302 | 309 | { 310 | this.props.autoplay && 311 | <> 312 | 319 | 327 | 328 | } 329 | 330 |
331 | ) 332 | } 333 | 334 | } 335 | 336 | export default withStandardBaseCssClass(withAuthorPanelSwitch(CarouselV1Impl), "cmp-carousel"); --------------------------------------------------------------------------------