diff --git a/.babelrc b/.babelrc index cd75116..02ab9ac 100644 --- a/.babelrc +++ b/.babelrc @@ -1,4 +1,7 @@ { - "presets": ["env", "react"], - "plugins": ["transform-object-rest-spread"] -} \ No newline at end of file + "presets": [ + "@babel/preset-env", + ["@babel/preset-react", { "runtime": "automatic" }] + ], + "plugins": ["@babel/plugin-proposal-object-rest-spread"] +} diff --git a/.eslintignore b/.eslintignore index 28e34ed..0fbae2a 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,2 +1,3 @@ README.md jest.config.js +docs/* diff --git a/.eslintrc b/.eslintrc index 431ab13..510450b 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,12 +1,17 @@ { + "root": true, + "parser": "babel-eslint", "env": { "browser": true, "es6": true, "jest/globals": true }, "extends": [ + "plugin:react/recommended", + "plugin:react-hooks/recommended", "eslint:recommended", - "plugin:react/recommended" + "plugin:jsx-a11y/recommended", + "react-app/jest" ], "settings": { "react": { @@ -14,39 +19,27 @@ } }, "parserOptions": { + "ecmaVersion": 11, "ecmaFeatures": { + "arrowFunctions": true, "experimentalObjectRestSpread": true, "jsx": true }, "sourceType": "module" }, - "plugins": [ - "react", - "jest" - ], + "plugins": ["react", "@babel", "jsx-a11y", "jest"], "rules": { - "indent": [ - "error", - "tab", - { - "SwitchCase": 1 - } - ], - "linebreak-style": [ - "error", - "unix" - ], + "indent": ["error", "tab"], + "linebreak-style": ["error", "unix"], "quotes": [ "error", - "single", + "double", { "allowTemplateLiterals": true } ], - "semi": [ - "error", - "always" - ], - "react/no-find-dom-node": "off" + "react/jsx-uses-react": "off", + "react/react-in-jsx-scope": "off", + "semi": ["error", "always"] } -} \ No newline at end of file +} diff --git a/.travis.yml b/.travis.yml index d4ae36c..553d2c6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,8 @@ language: node_js node_js: - - "10" + - "10" script: - - npm test - - npm run build + - npm test + - npm run build after_success: - - bash <(curl -s https://codecov.io/bash) + - bash <(curl -s https://codecov.io/bash) diff --git a/README.md b/README.md index 1f3f6ee..8e88d31 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![Dependency Status](https://david-dm.org/VictorCazanave/react-svg-map.svg)](https://david-dm.org/VictorCazanave/react-svg-map) [![peerDependencies Status](https://david-dm.org/VictorCazanave/react-svg-map/peer-status.svg)](https://david-dm.org/VictorCazanave/react-svg-map?type=peer) -_A set of React.js components to display an interactive SVG map._ +_A set of ReactJs components to display an interactive SVG map._ ![React SVG Map](https://media.giphy.com/media/QWpIwVdhY81RL05iNo/giphy.gif) @@ -42,14 +42,14 @@ This is the base component to display an SVG map. - Import the map you want - Optionally, import `react-svg-map/lib/index.css` if you want to apply the default styles -```javascript -import React from "react"; -import ReactDOM from "react-dom"; -import Taiwan from "@svg-maps/taiwan"; -import { SVGMap } from "react-svg-map"; -import "react-svg-map/lib/index.css"; +```jsx +import { PureComponent } from 'react'; +import ReactDOM from 'react-dom'; +import Taiwan from '@svg-maps/taiwan'; +import { SVGMap } from 'react-svg-map'; +import 'react-svg-map/lib/index.css'; -class App extends React.Component { +class App extends PureComponent { constructor(props) { super(props); } @@ -59,7 +59,7 @@ class App extends React.Component { } } -ReactDOM.render(, document.getElementById("app")); +ReactDOM.render(, document.getElementById('app')); ``` #### API @@ -69,6 +69,7 @@ ReactDOM.render(, document.getElementById("app")); | map | Object | **required** | Describe SVG map to display. See [maps section](#maps) for more details. | | className | String | `'svg-map'` | CSS class of ``. | | role | String | `'none'` | ARIA role of ``. | +| type | '`checkbox`' \| '`radio`' | '`radio`' | Defines the select method (single or multiple) of the location(s). Use `ref` to get the value of the map. | | locationClassName | String\|Function | `'svg-map__location'` | CSS class of each ``. The function parameters are the location object and the location index. | | locationTabIndex | String\|Function | `'0'` | Tabindex each ``. The function parameters are the location object and the location index. | | locationRole | String | `'none'` | ARIA role of each ``. | @@ -93,14 +94,14 @@ It is based on this [WAI-ARIA example](https://www.w3.org/TR/wai-aria-practices/ - Import the map you want - Optionally, import `react-svg-map/lib/index.css` if you want to apply the default styles -```javascript -import React from "react"; -import ReactDOM from "react-dom"; -import Taiwan from "@svg-maps/taiwan"; -import { CheckboxSVGMap } from "react-svg-map"; -import "react-svg-map/lib/index.css"; +```jsx +import { PureComponent } from 'react'; +import ReactDOM from 'react-dom'; +import Taiwan from '@svg-maps/taiwan'; +import { CheckboxSVGMap } from 'react-svg-map'; +import 'react-svg-map/lib/index.css'; -class App extends React.Component { +class App extends PureComponent { constructor(props) { super(props); } @@ -110,7 +111,7 @@ class App extends React.Component { } } -ReactDOM.render(, document.getElementById("app")); +ReactDOM.render(, document.getElementById('app')); ``` #### API @@ -121,8 +122,9 @@ ReactDOM.render(, document.getElementById("app")); | className | String | `'svg-map'` | CSS class of ``. | | locationClassName | String\|Function | `'svg-map__location'` | CSS class of each ``. The function parameters are the location object and the location index. | | locationAriaLabel | Function | `location.name` | ARIA label of each ``. The function parameters are the location object and the location index. | -| selectedLocationIds | String[] | | List of `id`s of the **initial** selected locations. It is used only when the component is mounted and is not synchronized when updated. | -| onChange | Function | | Invoked when the user selects/deselects a location. The list of selected locations is passed as parameter. | +| defaultValue | Location[] | [] | List of `Location` objects (`{ id: String, name: String }`) of the **initial** selected locations. It is used only when the component is mounted and is not synchronized when updated. | +| onChange | Function | | Invoked when the user selects/deselects a location. The attributes object of the toggled location is passed as parameter. | +| value | Location[] | | List of `Location` objects (`{ id: String, name: String }`). Used as a pair with `onChange` for controlled map. | | onLocationMouseOver | Function | | Invoked when the user puts the mouse over a location. | | onLocationMouseOut | Function | | Invoked when the user puts the mouse out of a location. | | onLocationMouseMove | Function | | Invoked when the user moves the mouse on a location. | @@ -140,14 +142,14 @@ It is based on this [WAI-ARIA example](https://www.w3.org/TR/wai-aria-practices/ - Import the map you want - Optionally, import `react-svg-map/lib/index.css` if you want to apply the default styles -```javascript -import React from "react"; -import ReactDOM from "react-dom"; -import Taiwan from "@svg-maps/taiwan"; -import { RadioSVGMap } from "react-svg-map"; -import "react-svg-map/lib/index.css"; +```jsx +import { PureComponent } from 'react'; +import ReactDOM from 'react-dom'; +import Taiwan from '@svg-maps/taiwan'; +import { RadioSVGMap } from 'react-svg-map'; +import 'react-svg-map/lib/index.css'; -class App extends React.Component { +class App extends PureComponent { constructor(props) { super(props); } @@ -157,7 +159,7 @@ class App extends React.Component { } } -ReactDOM.render(, document.getElementById("app")); +ReactDOM.render(, document.getElementById('app')); ``` #### API @@ -168,8 +170,9 @@ ReactDOM.render(, document.getElementById("app")); | className | String | `'svg-map'` | CSS class of ``. | | locationClassName | String\|Function | `'svg-map__location'` | CSS class of each ``. The function parameters are the location object and the location index. | | locationAriaLabel | Function | `location.name` | ARIA label of each ``. The function parameters are the location object and the location index. | -| selectedLocationId | String | | `id` of the **initial** selected location. It is used only when the component is mounted and is not synchronized when updated. | -| onChange | Function | | Invoked when the user selects a location. The selected location is passed as parameter. | +| defaultValue | Location | `null` | `Location` object (`{ id: String, name: String }`) of the **initial** selected location. It is used for the uncontrolled component map. | +| onChange | Function | | Invoked when the user selects a location. The attributes object of the selected location is passed as a parameter. | +| value | Location | | `Location` object (`{ id: String, name: String }`). Used as a pair with `onChange` for controlled map. | | onLocationMouseOver | Function | | Invoked when the user puts the mouse over a location. | | onLocationMouseOut | Function | | Invoked when the user puts the mouse out of a location. | | onLocationMouseMove | Function | | Invoked when the user moves the mouse on a location. | @@ -182,7 +185,7 @@ ReactDOM.render(, document.getElementById("app")); ### Existing maps -Since v2.0.0 this package does not provide maps anymore. All the existing maps have been moved to the independant [svg-maps](https://github.com/VictorCazanave/svg-maps) project because they may be useful for other components/projects. +Since v2.0.0 this package does not provide maps anymore. All the existing maps have been moved to the independent [svg-maps](https://github.com/VictorCazanave/svg-maps) project because they may be useful for other components/projects. ### Custom maps @@ -194,22 +197,22 @@ You can modify existing maps or create your own. 1. Create a new object from this map 1. Pass this new object as `map` prop of `` component -```javascript -import React from "react"; -import Taiwan from "@svg-maps/taiwan"; -import { SVGMap } from "react-svg-map"; +```jsx +import { PureComponent } from 'react'; +import Taiwan from '@svg-maps/taiwan'; +import { SVGMap } from 'react-svg-map'; -class App extends React.Component { +class App extends PureComponent { constructor(props) { super(props); // Create new map object this.customTaiwan = { ...Taiwan, - label: "Custom map label", + label: 'Custom map label', locations: Taiwan.locations.map(location => { // Modify each location - }) + }), }; } diff --git a/__tests__/__snapshots__/checkbox-svg-map.test.js.snap b/__tests__/__snapshots__/checkbox-svg-map.test.js.snap index 1618cb5..bc8b2c4 100644 --- a/__tests__/__snapshots__/checkbox-svg-map.test.js.snap +++ b/__tests__/__snapshots__/checkbox-svg-map.test.js.snap @@ -3,7 +3,7 @@ exports[`CheckboxSVGMap component Rendering displays map with custom props 1`] = ` @@ -98,15 +97,11 @@ exports[`CheckboxSVGMap component Rendering displays map with default props 1`] aria-label="name1" className="svg-map__location" d="path1" + data-testid="id1" id="id1" name="name1" - onBlur={undefined} onClick={[Function]} - onFocus={undefined} onKeyDown={[Function]} - onMouseMove={undefined} - onMouseOut={undefined} - onMouseOver={undefined} role="checkbox" tabIndex="0" /> @@ -115,15 +110,11 @@ exports[`CheckboxSVGMap component Rendering displays map with default props 1`] aria-label="name2" className="svg-map__location" d="path2" + data-testid="id2" id="id2" name="name2" - onBlur={undefined} onClick={[Function]} - onFocus={undefined} onKeyDown={[Function]} - onMouseMove={undefined} - onMouseOut={undefined} - onMouseOver={undefined} role="checkbox" tabIndex="0" /> diff --git a/__tests__/__snapshots__/radio-svg-map.test.js.snap b/__tests__/__snapshots__/radio-svg-map.test.js.snap index 3dc7ed9..eee043f 100644 --- a/__tests__/__snapshots__/radio-svg-map.test.js.snap +++ b/__tests__/__snapshots__/radio-svg-map.test.js.snap @@ -3,7 +3,7 @@ exports[`RadioSVGMap component Rendering displays map with custom props 1`] = ` @@ -98,15 +97,11 @@ exports[`RadioSVGMap component Rendering displays map with default props 1`] = ` aria-label="name1" className="svg-map__location" d="path1" + data-testid="id1" id="id1" name="name1" - onBlur={undefined} onClick={[Function]} - onFocus={undefined} onKeyDown={[Function]} - onMouseMove={undefined} - onMouseOut={undefined} - onMouseOver={undefined} role="radio" tabIndex="-1" /> @@ -115,15 +110,11 @@ exports[`RadioSVGMap component Rendering displays map with default props 1`] = ` aria-label="name2" className="svg-map__location" d="path2" + data-testid="id2" id="id2" name="name2" - onBlur={undefined} onClick={[Function]} - onFocus={undefined} onKeyDown={[Function]} - onMouseMove={undefined} - onMouseOut={undefined} - onMouseOver={undefined} role="radio" tabIndex="-1" /> diff --git a/__tests__/__snapshots__/svg-map.test.js.snap b/__tests__/__snapshots__/svg-map.test.js.snap index 454ca38..616e92a 100644 --- a/__tests__/__snapshots__/svg-map.test.js.snap +++ b/__tests__/__snapshots__/svg-map.test.js.snap @@ -9,53 +9,32 @@ exports[`SVGMap component Properties displays map with custom function location xmlns="http://www.w3.org/2000/svg" > @@ -65,7 +44,7 @@ exports[`SVGMap component Properties displays map with custom function location exports[`SVGMap component Properties displays map with custom props 1`] = ` diff --git a/__tests__/checkbox-svg-map.test.js b/__tests__/checkbox-svg-map.test.js index dd1d8cd..2edfbcc 100644 --- a/__tests__/checkbox-svg-map.test.js +++ b/__tests__/checkbox-svg-map.test.js @@ -1,99 +1,94 @@ -import React from 'react'; -import renderer from 'react-test-renderer'; -import { mount } from 'enzyme'; -import FakeMap from './fake-map'; -import { CheckboxSVGMap } from '../src'; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import renderer from "react-test-renderer"; + +import FakeMap from "./fake-map"; +import { CheckboxSVGMap } from "../src"; +import { getNodeAttributes } from "../src/utils"; // TODO: Try to make it more readable // TODO: Create utility functions to avoid code duplication -describe('CheckboxSVGMap component', () => { +describe("CheckboxSVGMap component", () => { let wrapper; - describe('Navigation', () => { - const locationSelector = '#id0'; + describe("Navigation", () => { + const locationSelector = "id0"; let location; beforeEach(() => { - wrapper = mount(); - location = wrapper.find(locationSelector); + wrapper = render(); + location = screen.getByTestId(locationSelector); }); afterEach(() => { wrapper.unmount(); }); - describe('Mouse', () => { - test('selects location when clicking on not yet selected location', () => { - expect(location.props()['aria-checked']).toBeFalsy(); + describe("Mouse", () => { + test("selects location when clicking on not yet selected location", () => { + expect(location).toHaveAttribute("aria-checked", "false"); - location.simulate('click'); - wrapper.update(); - location = wrapper.find(locationSelector); + userEvent.click(location); + location = screen.getByTestId(locationSelector); - expect(location.props()['aria-checked']).toBeTruthy(); + expect(location).toHaveAttribute("aria-checked", "true"); }); - test('deselects location when clicking on already selected location', () => { - location.simulate('click'); - wrapper.update(); - location = wrapper.find(locationSelector); + test("deselects location when clicking on already selected location", () => { + userEvent.click(location); + location = screen.getByTestId(locationSelector); - expect(location.props()['aria-checked']).toBeTruthy(); + expect(location).toHaveAttribute("aria-checked", "true"); - location.simulate('click'); - wrapper.update(); - location = wrapper.find(locationSelector); + userEvent.click(location); + location = screen.getByTestId(locationSelector); - expect(location.props()['aria-checked']).toBeFalsy(); + expect(location).toHaveAttribute("aria-checked", "false"); }); }); - describe('Keyboard', () => { - test('selects focused location when hitting spacebar', () => { - expect(location.props()['aria-checked']).toBeFalsy(); + describe("Keyboard", () => { + test("selects focused location when hitting spacebar", () => { + expect(location).toHaveAttribute("aria-checked", "false"); - location.simulate('focus'); - location.simulate('keydown', { keyCode: 32 }); - wrapper.update(); - location = wrapper.find(locationSelector); + location.focus(); + userEvent.keyboard("{space}"); + location = screen.getByTestId(locationSelector); - expect(location.props()['aria-checked']).toBeTruthy(); + expect(location).toHaveAttribute("aria-checked", "true"); }); - test('does not select focused location when hitting other key', () => { - expect(location.props()['aria-checked']).toBeFalsy(); + test("does not select focused location when hitting other key", () => { + expect(location).toHaveAttribute("aria-checked", "false"); - location.simulate('focus'); - location.simulate('keydown', { keyCode: 31 }); - wrapper.update(); - location = wrapper.find(locationSelector); + location.focus(); + userEvent.keyboard("{Key0}"); + location = screen.getByTestId(locationSelector); - expect(location.props()['aria-checked']).toBeFalsy(); + expect(location).toHaveAttribute("aria-checked", "false"); }); - test('deselects focused already selected location when hitting spacebar', () => { - location.simulate('focus'); - location.simulate('keydown', { keyCode: 32 }); - wrapper.update(); - location = wrapper.find(locationSelector); + test("deselects focused already selected location when hitting spacebar", () => { + location.focus(); + userEvent.keyboard("{space}"); + location = screen.getByTestId(locationSelector); - expect(location.props()['aria-checked']).toBeTruthy(); + expect(location).toHaveAttribute("aria-checked", "true"); - location.simulate('focus'); - location.simulate('keydown', { keyCode: 32 }); - wrapper.update(); - location = wrapper.find(locationSelector); + location.focus(); + userEvent.keyboard("{space}"); + location = screen.getByTestId(locationSelector); - expect(location.props()['aria-checked']).toBeFalsy(); + expect(location).toHaveAttribute("aria-checked", "false"); }); }); }); - describe('Communication', () => { + describe("Communication", () => { // Create element to attach component to it and avoid warnings when attached to document.body // https://stackoverflow.com/a/49025532/9826498 - const container = document.createElement('div'); + const container = document.createElement("div"); document.body.appendChild(container); const handleOnChange = jest.fn(); @@ -103,17 +98,17 @@ describe('CheckboxSVGMap component', () => { let unselectedLocation; beforeEach(() => { - wrapper = mount( + wrapper = render( , { attachTo: container } ); - selectedLocation = wrapper.find('#id0'); - otherSelectedLocation = wrapper.find('#id1'); - unselectedLocation = wrapper.find('#id2'); + selectedLocation = screen.getByTestId("id0"); + otherSelectedLocation = screen.getByTestId("id1"); + unselectedLocation = screen.getByTestId("id2"); }); afterEach(() => { @@ -121,41 +116,46 @@ describe('CheckboxSVGMap component', () => { handleOnChange.mockClear(); }); - test('selects initial locations when valid ids are provided', () => { - expect(selectedLocation.props()['aria-checked']).toBeTruthy(); - expect(otherSelectedLocation.props()['aria-checked']).toBeTruthy(); - expect(unselectedLocation.props()['aria-checked']).toBeFalsy(); + test("selects initial locations when valid ids are provided", () => { + expect(selectedLocation).toHaveAttribute("aria-checked", "true"); + expect(otherSelectedLocation).toHaveAttribute("aria-checked", "true"); + expect(unselectedLocation).toHaveAttribute("aria-checked", "false"); }); - test('calls onChange handler when selecting location', () => { - unselectedLocation.simulate('click'); + test("calls onChange handler when selecting location", () => { + userEvent.click(unselectedLocation); + const unselectedLocationAttributes = + getNodeAttributes(unselectedLocation); - expect(handleOnChange).toHaveBeenCalledWith([ - selectedLocation.getDOMNode(), - otherSelectedLocation.getDOMNode(), - unselectedLocation.getDOMNode() - ]); + expect(handleOnChange).toHaveBeenCalledWith(unselectedLocationAttributes); }); - test('calls onChange handler when deselecting location', () => { - otherSelectedLocation.simulate('click'); + test("calls onChange handler when deselecting location", () => { + userEvent.click(otherSelectedLocation); + const otherSelectedLocationAttributes = getNodeAttributes( + otherSelectedLocation + ); - expect(handleOnChange).toHaveBeenCalledWith([selectedLocation.getDOMNode()]); + expect(handleOnChange).toHaveBeenCalledWith( + otherSelectedLocationAttributes + ); }); }); - describe('Rendering', () => { - test('displays map with default props', () => { + describe("Rendering", () => { + test("displays map with default props", () => { const component = renderer.create(); const tree = component.toJSON(); expect(tree).toMatchSnapshot(); }); - test('displays map with custom props', () => { - const eventHandler = () => 'eventHandler'; + test("displays map with custom props", () => { + const eventHandler = () => "eventHandler"; + const value = []; const component = renderer.create( - { onLocationFocus={eventHandler} onLocationBlur={eventHandler} onChange={eventHandler} + value={value} childrenBefore={childrenBefore} childrenAfter={childrenAfter} /> diff --git a/__tests__/fake-map.js b/__tests__/fake-map.js index 21fc65d..83c29c5 100644 --- a/__tests__/fake-map.js +++ b/__tests__/fake-map.js @@ -1,21 +1,21 @@ export default { - label: 'label', - viewBox: 'viewBox', + label: "label", + viewBox: "viewBox", locations: [ { - name: 'name0', - id: 'id0', - path: 'path0' + name: "name0", + id: "id0", + path: "path0" }, { - name: 'name1', - id: 'id1', - path: 'path1' + name: "name1", + id: "id1", + path: "path1" }, { - name: 'name2', - id: 'id2', - path: 'path2' + name: "name2", + id: "id2", + path: "path2" } ] }; diff --git a/__tests__/radio-svg-map.test.js b/__tests__/radio-svg-map.test.js index bf30ffb..95d147c 100644 --- a/__tests__/radio-svg-map.test.js +++ b/__tests__/radio-svg-map.test.js @@ -1,164 +1,151 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import renderer from 'react-test-renderer'; -import { mount } from 'enzyme'; -import FakeMap from './fake-map'; -import { RadioSVGMap } from '../src'; +import ReactDOM from "react-dom"; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import renderer from "react-test-renderer"; + +import FakeMap from "./fake-map"; +import { RadioSVGMap } from "../src"; +import { getNodeAttributes } from "../src/utils"; // TODO: Try to make it more readable // TODO: Create utility functions to avoid code duplication -describe('RadioSVGMap component', () => { - const locationSelector = '#id1'; - const previousLocationSelector = '#id0'; - const nextLocationSelector = '#id2'; +describe("RadioSVGMap component", () => { + const locationSelector = "id1"; + const previousLocationSelector = "id0"; + const nextLocationSelector = "id2"; let wrapper; let location; let previousLocation; let nextLocation; - describe('Navigation', () => { + describe("Navigation", () => { beforeEach(() => { - wrapper = mount(); - location = wrapper.find(locationSelector); - previousLocation = wrapper.find(previousLocationSelector); - nextLocation = wrapper.find(nextLocationSelector); + wrapper = render(); + location = screen.getByTestId(locationSelector); + previousLocation = screen.getByTestId(previousLocationSelector); + nextLocation = screen.getByTestId(nextLocationSelector); }); afterEach(() => { wrapper.unmount(); }); - describe('Mouse', () => { - test('selects location when clicking on not yet selected location', () => { - expect(location.props()['aria-checked']).toBeFalsy(); + describe("Mouse", () => { + it("selects location when clicking on not yet selected location", () => { + expect(location.getAttribute("aria-checked")).toBe(null); - location.simulate('click'); - wrapper.update(); - location = wrapper.find(locationSelector); + userEvent.click(location); + location = screen.getByTestId(locationSelector); - expect(location.props()['aria-checked']).toBeTruthy(); + expect(location).toHaveAttribute("aria-checked"); }); - test('does not deselect location when clicking on already selected location', () => { - location.simulate('click'); - wrapper.update(); - location = wrapper.find(locationSelector); + it("does not deselect location when clicking on already selected location", () => { + userEvent.click(location); + location = screen.getByTestId(locationSelector); - expect(location.props()['aria-checked']).toBeTruthy(); + expect(location).toHaveAttribute("aria-checked", "true"); - location.simulate('click'); - wrapper.update(); - location = wrapper.find(locationSelector); + userEvent.click(location); + location = screen.getByTestId(locationSelector); - expect(location.props()['aria-checked']).toBeTruthy(); + expect(location).toHaveAttribute("aria-checked", "true"); }); - test('selects new location and deselects former selected when clicking on new location', () => { - location.simulate('click'); - wrapper.update(); - location = wrapper.find(locationSelector); + it("selects new location and deselects former selected when clicking on new location", () => { + userEvent.click(location); + location = screen.getByTestId(locationSelector); - expect(location.props()['aria-checked']).toBeTruthy(); - expect(previousLocation.props()['aria-checked']).toBeFalsy(); + expect(location).toHaveAttribute("aria-checked", "true"); + expect(previousLocation).toHaveAttribute("aria-checked", "false"); - previousLocation.simulate('click'); - wrapper.update(); - location = wrapper.find(locationSelector); - previousLocation = wrapper.find(previousLocationSelector); + userEvent.click(previousLocation); + location = screen.getByTestId(locationSelector); + previousLocation = screen.getByTestId(previousLocationSelector); - expect(location.props()['aria-checked']).toBeFalsy(); - expect(previousLocation.props()['aria-checked']).toBeTruthy(); + expect(location).toHaveAttribute("aria-checked", "false"); + expect(previousLocation).toHaveAttribute("aria-checked", "true"); }); - test('makes location focusable when selected', () => { - expect(location.props()['tabIndex']).toEqual('-1'); + it("makes location focusable when selected", () => { + expect(location).toHaveProperty("tabIndex", -1); - location.simulate('click'); - wrapper.update(); - location = wrapper.find(locationSelector); + userEvent.click(location); + location = screen.getByTestId(locationSelector); - expect(location.props()['tabIndex']).toEqual('0'); + expect(location).toHaveProperty("tabIndex", 0); }); }); - describe('Keyboard', () => { - test('selects focused not yet selected location when hitting spacebar', () => { - expect(location.props()['aria-checked']).toBeFalsy(); + describe("Keyboard", () => { + it("selects focused not yet selected location when hitting spacebar", () => { + expect(location.getAttribute("aria-checked")).toBe(null); - location.simulate('focus'); - location.simulate('keydown', { keyCode: 32 }); - wrapper.update(); - location = wrapper.find(locationSelector); + location.focus(); + userEvent.keyboard("{space}"); + location = screen.getByTestId(locationSelector); - expect(location.props()['aria-checked']).toBeTruthy(); + expect(location).toHaveAttribute("aria-checked", "true"); }); - test('does not deselect focused already selected location when hitting spacebar', () => { - location.simulate('focus'); - location.simulate('keydown', { keyCode: 32 }); - wrapper.update(); - location = wrapper.find(locationSelector); + it("does not deselect focused already selected location when hitting spacebar", () => { + location.focus(); + userEvent.keyboard("{space}"); + location = screen.getByTestId(locationSelector); - expect(location.props()['aria-checked']).toBeTruthy(); + expect(location).toHaveAttribute("aria-checked", "true"); - location.simulate('focus'); - location.simulate('keydown', { keyCode: 32 }); - wrapper.update(); - location = wrapper.find(locationSelector); + location.focus(); + userEvent.keyboard("{space}"); + location = screen.getByTestId(locationSelector); - expect(location.props()['aria-checked']).toBeTruthy(); + expect(location).toHaveAttribute("aria-checked", "true"); }); - test('selects next/first location when hitting down/right arrow', () => { - expect(location.props()['aria-checked']).toBeFalsy(); - expect(nextLocation.props()['aria-checked']).toBeFalsy(); + it("selects next/first location when hitting down/right arrow", () => { + expect(location.getAttribute("aria-checked")).toBe(null); + expect(nextLocation.getAttribute("aria-checked")).toBe(null); - location.simulate('focus'); - location.simulate('keydown', { keyCode: 39 }); - wrapper.update(); - location = wrapper.find(locationSelector); - nextLocation = wrapper.find(nextLocationSelector); + location.focus(); + userEvent.keyboard("{arrowright}"); + location = screen.getByTestId(locationSelector); + nextLocation = screen.getByTestId(nextLocationSelector); - expect(location.props()['aria-checked']).toBeFalsy(); - expect(nextLocation.props()['aria-checked']).toBeTruthy(); + expect(location).toHaveAttribute("aria-checked", "false"); + expect(nextLocation).toHaveAttribute("aria-checked", "true"); }); - test('selects previous/last location when hitting up/left arrow', () => { - expect(location.props()['aria-checked']).toBeFalsy(); - expect(previousLocation.props()['aria-checked']).toBeFalsy(); + it("selects previous/last location when hitting up/left arrow", () => { + expect(location.getAttribute("aria-checked")).toBe(null); - location.simulate('focus'); - location.simulate('keydown', { keyCode: 37 }); - wrapper.update(); - location = wrapper.find(locationSelector); - previousLocation = wrapper.find(previousLocationSelector); + location.focus(); + userEvent.keyboard("{arrowleft}"); + location = screen.getByTestId(locationSelector); + previousLocation = screen.getByTestId(previousLocationSelector); - expect(location.props()['aria-checked']).toBeFalsy(); - expect(previousLocation.props()['aria-checked']).toBeTruthy(); + expect(location).toHaveAttribute("aria-checked", "false"); + expect(previousLocation).toHaveAttribute("aria-checked", "true"); }); }); }); - describe('Communication', () => { + describe("Communication", () => { // Create element to attach component to it and avoid warnings when attached to document.body // https://stackoverflow.com/a/49025532/9826498 - const container = document.createElement('div'); + const container = document.createElement("div"); document.body.appendChild(container); const handleOnChange = jest.fn(); + const value = { id: "id1" }; beforeEach(() => { - wrapper = mount( - , + wrapper = render( + , { attachTo: container } ); - location = wrapper.find(locationSelector); - nextLocation = wrapper.find(nextLocationSelector); + location = screen.getByTestId(locationSelector); + nextLocation = screen.getByTestId(nextLocationSelector); }); afterEach(() => { @@ -166,44 +153,45 @@ describe('RadioSVGMap component', () => { handleOnChange.mockClear(); }); - test('selects initial location when id is provided', () => { - expect(location.props()['aria-checked']).toBeTruthy(); + it("selects initial location when id is provided", () => { + expect(location).toHaveAttribute("aria-checked", "true"); }); - test('calls onChange handler when selecting location', () => { - nextLocation.simulate('click'); + it("calls onChange handler when selecting location", () => { + userEvent.click(nextLocation); + const nextLocationAttributes = getNodeAttributes(nextLocation); - expect(handleOnChange).toHaveBeenCalledWith(nextLocation.getDOMNode()); + expect(handleOnChange).toHaveBeenCalledWith(nextLocationAttributes); }); - test('does not call onChange handler when clicking on already selected location', () => { - location.simulate('click'); + it("does not call onChange handler when clicking on already selected location", () => { + userEvent.click(location); expect(handleOnChange).toHaveBeenCalledTimes(0); }); }); - describe('Rendering', () => { + describe("Rendering", () => { beforeAll(() => { // Mock ReactDOM to avoid error - ReactDOM.findDOMNode = jest.fn( - () => ({ - getElementsByTagName: jest.fn(() => ([])) - }) - ); + ReactDOM.findDOMNode = jest.fn(() => ({ + getElementsByTagName: jest.fn(() => []), + })); }); - test('displays map with default props', () => { + it("displays map with default props", () => { const component = renderer.create(); const tree = component.toJSON(); expect(tree).toMatchSnapshot(); }); - test('displays map with custom props', () => { - const eventHandler = () => 'eventHandler'; + it("displays map with custom props", () => { + const eventHandler = () => "eventHandler"; + const value = null; const component = renderer.create( - { onLocationFocus={eventHandler} onLocationBlur={eventHandler} onChange={eventHandler} + value={value} childrenBefore={childrenBefore} childrenAfter={childrenAfter} /> diff --git a/__tests__/svg-map.test.js b/__tests__/svg-map.test.js index db69f82..b6ea359 100644 --- a/__tests__/svg-map.test.js +++ b/__tests__/svg-map.test.js @@ -1,24 +1,26 @@ -import React from 'react'; -import renderer from 'react-test-renderer'; -import FakeMap from './fake-map'; -import { SVGMap } from '../src/'; - -describe('SVGMap component', () => { - describe('Properties', () => { - test('displays map with default props', () => { +import renderer from "react-test-renderer"; + +import FakeMap from "./fake-map"; +import { SVGMap } from "../src"; + +describe("SVGMap component", () => { + describe("Properties", () => { + it("displays map with default props", () => { const component = renderer.create(); const tree = component.toJSON(); expect(tree).toMatchSnapshot(); }); - test('displays map with custom props', () => { - const eventHandler = () => 'eventHandler'; - const isLocationSelected = () => 'isLocationSelected'; + it("displays map with custom props", () => { + const eventHandler = () => "eventHandler"; + const isLocationSelected = () => "isLocationSelected"; + const component = renderer.create( - { expect(tree).toMatchSnapshot(); }); - test('displays map with custom function location props', () => { - const locationClassName = (location, index) => `locationClassName-${index}`; + it("displays map with custom function location props", () => { + const locationClassName = (location, index) => + `locationClassName-${index}`; const locationTabIndex = (location, index) => `locationTabIndex-${index}`; - const locationAriaLabel = (location, index) => `${location.name}-${index}`; + const locationAriaLabel = (location, index) => + `${location.name}-${index}`; + const component = renderer.create( - Examples of react-svg-map
\ No newline at end of file +Examples of react-svg-map
\ No newline at end of file diff --git a/docs/index.js b/docs/index.js index 8fb689d..18633d9 100644 --- a/docs/index.js +++ b/docs/index.js @@ -1,31 +1,2 @@ -!function(l){var e={};function t(n){if(e[n])return e[n].exports;var L=e[n]={i:n,l:!1,exports:{}};return l[n].call(L.exports,L,L.exports,t),L.l=!0,L.exports}t.m=l,t.c=e,t.d=function(l,e,n){t.o(l,e)||Object.defineProperty(l,e,{enumerable:!0,get:n})},t.r=function(l){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(l,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(l,"__esModule",{value:!0})},t.t=function(l,e){if(1&e&&(l=t(l)),8&e)return l;if(4&e&&"object"==typeof l&&l&&l.__esModule)return l;var n=Object.create(null);if(t.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:l}),2&e&&"string"!=typeof l)for(var L in l)t.d(n,L,function(e){return l[e]}.bind(null,L));return n},t.n=function(l){var e=l&&l.__esModule?function(){return l.default}:function(){return l};return t.d(e,"a",e),e},t.o=function(l,e){return Object.prototype.hasOwnProperty.call(l,e)},t.p="",t(t.s=9)}([function(l,e,t){"use strict";l.exports=t(10)},function(l,e,t){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.RadioSVGMap=e.CheckboxSVGMap=e.SVGMap=void 0;var n=r(t(3)),L=r(t(20)),o=r(t(21));function r(l){return l&&l.__esModule?l:{default:l}}e.SVGMap=n.default,e.CheckboxSVGMap=L.default,e.RadioSVGMap=o.default},function(l,e,t){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.getLocationId=function(l){return l.target.id},e.getLocationName=function(l){return l.target.attributes.name.value},e.getLocationSelected=function(l){return"true"===l.target.attributes["aria-checked"].value}},function(l,e,t){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var n=o(t(0)),L=o(t(4));function o(l){return l&&l.__esModule?l:{default:l}}function r(l){return n.default.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:l.map.viewBox,className:l.className,role:l.role,"aria-label":l.map.label},l.map.locations.map(function(e,t){return n.default.createElement("path",{id:e.id,name:e.name,d:e.path,className:"function"==typeof l.locationClassName?l.locationClassName(e,t):l.locationClassName,tabIndex:"function"==typeof l.locationTabIndex?l.locationTabIndex(e,t):l.locationTabIndex,role:l.locationRole,"aria-label":e.name,"aria-checked":l.isLocationSelected&&l.isLocationSelected(e,t),onMouseOver:l.onLocationMouseOver,onMouseOut:l.onLocationMouseOut,onMouseMove:l.onLocationMouseMove,onClick:l.onLocationClick,onKeyDown:l.onLocationKeyDown,onFocus:l.onLocationFocus,onBlur:l.onLocationBlur,key:e.id})}))}r.propTypes={map:L.default.shape({viewBox:L.default.string.isRequired,locations:L.default.arrayOf(L.default.shape({path:L.default.string.isRequired,id:L.default.string.isRequired,name:L.default.string})).isRequired,label:L.default.string}).isRequired,className:L.default.string,role:L.default.string,locationClassName:L.default.oneOfType([L.default.string,L.default.func]),locationTabIndex:L.default.oneOfType([L.default.string,L.default.func]),locationRole:L.default.string,onLocationMouseOver:L.default.func,onLocationMouseOut:L.default.func,onLocationMouseMove:L.default.func,onLocationClick:L.default.func,onLocationKeyDown:L.default.func,onLocationFocus:L.default.func,onLocationBlur:L.default.func,isLocationSelected:L.default.func},r.defaultProps={className:"svg-map",role:"none",locationClassName:"svg-map__location"},e.default=r},function(l,e,t){l.exports=t(18)()},function(l,e,t){"use strict"; -/* -object-assign -(c) Sindre Sorhus -@license MIT -*/var n=Object.getOwnPropertySymbols,L=Object.prototype.hasOwnProperty,o=Object.prototype.propertyIsEnumerable;l.exports=function(){try{if(!Object.assign)return!1;var l=new String("abc");if(l[5]="de","5"===Object.getOwnPropertyNames(l)[0])return!1;for(var e={},t=0;t<10;t++)e["_"+String.fromCharCode(t)]=t;if("0123456789"!==Object.getOwnPropertyNames(e).map(function(l){return e[l]}).join(""))return!1;var n={};return"abcdefghijklmnopqrst".split("").forEach(function(l){n[l]=l}),"abcdefghijklmnopqrst"===Object.keys(Object.assign({},n)).join("")}catch(l){return!1}}()?Object.assign:function(l,e){for(var t,r,a=function(l){if(null==l)throw new TypeError("Object.assign cannot be called with null or undefined");return Object(l)}(l),i=1;i=0&&c.splice(e,1)}function h(l){var e=document.createElement("style");return l.attrs.type="text/css",y(e,l.attrs),p(l,e),e}function y(l,e){Object.keys(e).forEach(function(t){l.setAttribute(t,e[t])})}function v(l,e){var t,n,L,o;if(e.transform&&l.css){if(!(o=e.transform(l.css)))return function(){};l.css=o}if(e.singleton){var r=u++;t=i||(i=h(e)),n=_.bind(null,t,r,!1),L=_.bind(null,t,r,!0)}else l.sourceMap&&"function"==typeof URL&&"function"==typeof URL.createObjectURL&&"function"==typeof URL.revokeObjectURL&&"function"==typeof Blob&&"function"==typeof btoa?(t=function(l){var e=document.createElement("link");return l.attrs.type="text/css",l.attrs.rel="stylesheet",y(e,l.attrs),p(l,e),e}(e),n=function(l,e,t){var n=t.css,L=t.sourceMap,o=void 0===e.convertToAbsoluteUrls&&L;(e.convertToAbsoluteUrls||o)&&(n=s(n));L&&(n+="\n/*# sourceMappingURL=data:application/json;base64,"+btoa(unescape(encodeURIComponent(JSON.stringify(L))))+" */");var r=new Blob([n],{type:"text/css"}),a=l.href;l.href=URL.createObjectURL(r),a&&URL.revokeObjectURL(a)}.bind(null,t,e),L=function(){m(t),t.href&&URL.revokeObjectURL(t.href)}):(t=h(e),n=function(l,e){var t=e.css,n=e.media;n&&l.setAttribute("media",n);if(l.styleSheet)l.styleSheet.cssText=t;else{for(;l.firstChild;)l.removeChild(l.firstChild);l.appendChild(document.createTextNode(t))}}.bind(null,t),L=function(){m(t)});return n(l),function(e){if(e){if(e.css===l.css&&e.media===l.media&&e.sourceMap===l.sourceMap)return;n(l=e)}else L()}}l.exports=function(l,e){if("undefined"!=typeof DEBUG&&DEBUG&&"object"!=typeof document)throw new Error("The style-loader cannot be used in a non-browser environment");(e=e||{}).attrs="object"==typeof e.attrs?e.attrs:{},e.singleton||"boolean"==typeof e.singleton||(e.singleton=r()),e.insertInto||(e.insertInto="head"),e.insertAt||(e.insertAt="bottom");var t=d(l,e);return f(t,e),function(l){for(var n=[],L=0;LO.length&&O.push(l)}function I(l,e,t){return null==l?0:function l(e,t,n,L){var a=typeof e;"undefined"!==a&&"boolean"!==a||(e=null);var i=!1;if(null===e)i=!0;else switch(a){case"string":case"number":i=!0;break;case"object":switch(e.$$typeof){case o:case r:i=!0}}if(i)return n(L,e,""===t?"."+j(e,0):t),1;if(i=0,t=""===t?".":t+":",Array.isArray(e))for(var u=0;uthis.eventPool.length&&this.eventPool.push(l)}function sl(l){l.eventPool=[],l.getPooled=ul,l.release=cl}L(il.prototype,{preventDefault:function(){this.defaultPrevented=!0;var l=this.nativeEvent;l&&(l.preventDefault?l.preventDefault():"unknown"!=typeof l.returnValue&&(l.returnValue=!1),this.isDefaultPrevented=rl)},stopPropagation:function(){var l=this.nativeEvent;l&&(l.stopPropagation?l.stopPropagation():"unknown"!=typeof l.cancelBubble&&(l.cancelBubble=!0),this.isPropagationStopped=rl)},persist:function(){this.isPersistent=rl},isPersistent:al,destructor:function(){var l,e=this.constructor.Interface;for(l in e)this[l]=null;this.nativeEvent=this._targetInst=this.dispatchConfig=null,this.isPropagationStopped=this.isDefaultPrevented=al,this._dispatchInstances=this._dispatchListeners=null}}),il.Interface={type:null,target:null,currentTarget:function(){return null},eventPhase:null,bubbles:null,cancelable:null,timeStamp:function(l){return l.timeStamp||Date.now()},defaultPrevented:null,isTrusted:null},il.extend=function(l){function e(){}function t(){return n.apply(this,arguments)}var n=this;e.prototype=n.prototype;var o=new e;return L(o,t.prototype),t.prototype=o,t.prototype.constructor=t,t.Interface=L({},n.Interface,l),t.extend=n.extend,sl(t),t},sl(il);var fl=il.extend({data:null}),dl=il.extend({data:null}),pl=[9,13,27,32],ml=$&&"CompositionEvent"in window,hl=null;$&&"documentMode"in document&&(hl=document.documentMode);var yl=$&&"TextEvent"in window&&!hl,vl=$&&(!ml||hl&&8=hl),gl=String.fromCharCode(32),bl={beforeInput:{phasedRegistrationNames:{bubbled:"onBeforeInput",captured:"onBeforeInputCapture"},dependencies:["compositionend","keypress","textInput","paste"]},compositionEnd:{phasedRegistrationNames:{bubbled:"onCompositionEnd",captured:"onCompositionEndCapture"},dependencies:"blur compositionend keydown keypress keyup mousedown".split(" ")},compositionStart:{phasedRegistrationNames:{bubbled:"onCompositionStart",captured:"onCompositionStartCapture"},dependencies:"blur compositionstart keydown keypress keyup mousedown".split(" ")},compositionUpdate:{phasedRegistrationNames:{bubbled:"onCompositionUpdate",captured:"onCompositionUpdateCapture"},dependencies:"blur compositionupdate keydown keypress keyup mousedown".split(" ")}},_l=!1;function kl(l,e){switch(l){case"keyup":return-1!==pl.indexOf(e.keyCode);case"keydown":return 229!==e.keyCode;case"keypress":case"mousedown":case"blur":return!0;default:return!1}}function wl(l){return"object"==typeof(l=l.detail)&&"data"in l?l.data:null}var xl=!1;var Ml={eventTypes:bl,extractEvents:function(l,e,t,n){var L=void 0,o=void 0;if(ml)l:{switch(l){case"compositionstart":L=bl.compositionStart;break l;case"compositionend":L=bl.compositionEnd;break l;case"compositionupdate":L=bl.compositionUpdate;break l}L=void 0}else xl?kl(l,t)&&(L=bl.compositionEnd):"keydown"===l&&229===t.keyCode&&(L=bl.compositionStart);return L?(vl&&"ko"!==t.locale&&(xl||L!==bl.compositionStart?L===bl.compositionEnd&&xl&&(o=ol()):(nl="value"in(tl=n)?tl.value:tl.textContent,xl=!0)),L=fl.getPooled(L,e,t,n),o?L.data=o:null!==(o=wl(t))&&(L.data=o),H(L),o=L):o=null,(l=yl?function(l,e){switch(l){case"compositionend":return wl(e);case"keypress":return 32!==e.which?null:(_l=!0,gl);case"textInput":return(l=e.data)===gl&&_l?null:l;default:return null}}(l,t):function(l,e){if(xl)return"compositionend"===l||!ml&&kl(l,e)?(l=ol(),Ll=nl=tl=null,xl=!1,l):null;switch(l){case"paste":return null;case"keypress":if(!(e.ctrlKey||e.altKey||e.metaKey)||e.ctrlKey&&e.altKey){if(e.char&&1