@khanacademy/wonder-blocks-clickable 2.1.2 → 2.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/dist/es/index.js +27 -25
- package/dist/index.js +593 -286
- package/package.json +9 -10
- package/src/components/__tests__/clickable-behavior.test.js +47 -2
- package/src/components/__tests__/clickable.test.js +19 -12
- package/src/components/clickable-behavior.js +9 -9
- package/src/components/clickable.js +16 -13
- package/src/components/clickable.md +7 -0
- package/src/components/clickable.stories.js +8 -10
- package/src/util/get-clickable-behavior.js +2 -2
- package/LICENSE +0 -21
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@khanacademy/wonder-blocks-clickable",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.2",
|
|
4
4
|
"design": "v1",
|
|
5
5
|
"description": "Clickable component for Wonder-Blocks.",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -15,18 +15,17 @@
|
|
|
15
15
|
"access": "public"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@babel/runtime": "^7.
|
|
19
|
-
"@khanacademy/wonder-blocks-core": "^
|
|
18
|
+
"@babel/runtime": "^7.16.3",
|
|
19
|
+
"@khanacademy/wonder-blocks-core": "^4.0.0"
|
|
20
20
|
},
|
|
21
21
|
"peerDependencies": {
|
|
22
22
|
"aphrodite": "^1.2.5",
|
|
23
|
-
"
|
|
24
|
-
"react": "
|
|
25
|
-
"react-
|
|
26
|
-
"react-router-dom": "
|
|
23
|
+
"react": "16.14.0",
|
|
24
|
+
"react-dom": "16.14.0",
|
|
25
|
+
"react-router": "5.2.1",
|
|
26
|
+
"react-router-dom": "5.3.0"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
|
-
"wb-dev-build-settings": "^0.
|
|
30
|
-
}
|
|
31
|
-
"gitHead": "8022bb419eed74be37f71f71c7621854794a731c"
|
|
29
|
+
"wb-dev-build-settings": "^0.2.0"
|
|
30
|
+
}
|
|
32
31
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/* eslint-disable max-lines */
|
|
2
2
|
// @flow
|
|
3
3
|
import * as React from "react";
|
|
4
|
+
import {render, screen} from "@testing-library/react";
|
|
4
5
|
import {MemoryRouter, Switch, Route} from "react-router-dom";
|
|
5
6
|
import {mount, shallow} from "enzyme";
|
|
7
|
+
import "jest-enzyme";
|
|
6
8
|
|
|
7
9
|
import getClickableBehavior from "../../util/get-clickable-behavior.js";
|
|
8
10
|
import ClickableBehavior from "../clickable-behavior.js";
|
|
@@ -22,8 +24,9 @@ const wait = (delay: number = 0) =>
|
|
|
22
24
|
describe("ClickableBehavior", () => {
|
|
23
25
|
beforeEach(() => {
|
|
24
26
|
// Note: window.location.assign and window.open need mock functions in
|
|
25
|
-
// the testing environment
|
|
26
|
-
window.location
|
|
27
|
+
// the testing environment
|
|
28
|
+
delete window.location;
|
|
29
|
+
window.location = {assign: jest.fn()};
|
|
27
30
|
window.open = jest.fn();
|
|
28
31
|
});
|
|
29
32
|
|
|
@@ -274,6 +277,46 @@ describe("ClickableBehavior", () => {
|
|
|
274
277
|
expect(button.state("focused")).toEqual(false);
|
|
275
278
|
});
|
|
276
279
|
|
|
280
|
+
test("tabIndex should be 0", () => {
|
|
281
|
+
// Arrange
|
|
282
|
+
// Act
|
|
283
|
+
render(
|
|
284
|
+
<ClickableBehavior disabled={false} onClick={(e) => {}}>
|
|
285
|
+
{(state, childrenProps) => {
|
|
286
|
+
return (
|
|
287
|
+
<button data-test-id="test-button-1" {...childrenProps}>
|
|
288
|
+
Label
|
|
289
|
+
</button>
|
|
290
|
+
);
|
|
291
|
+
}}
|
|
292
|
+
</ClickableBehavior>,
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
// Assert
|
|
296
|
+
const button = screen.getByTestId("test-button-1");
|
|
297
|
+
expect(button).toHaveAttribute("tabIndex", "0");
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test("tabIndex should be 0 even for disabled components", () => {
|
|
301
|
+
// Arrange
|
|
302
|
+
// Act
|
|
303
|
+
render(
|
|
304
|
+
<ClickableBehavior disabled={true} onClick={(e) => {}}>
|
|
305
|
+
{(state, childrenProps) => {
|
|
306
|
+
return (
|
|
307
|
+
<button data-test-id="test-button-2" {...childrenProps}>
|
|
308
|
+
Label
|
|
309
|
+
</button>
|
|
310
|
+
);
|
|
311
|
+
}}
|
|
312
|
+
</ClickableBehavior>,
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
// Assert
|
|
316
|
+
const button = screen.getByTestId("test-button-2");
|
|
317
|
+
expect(button).toHaveAttribute("tabIndex", "0");
|
|
318
|
+
});
|
|
319
|
+
|
|
277
320
|
it("does not change state if disabled", () => {
|
|
278
321
|
const onClick = jest.fn();
|
|
279
322
|
const button = shallow(
|
|
@@ -566,6 +609,7 @@ describe("ClickableBehavior", () => {
|
|
|
566
609
|
const button = wrapper.find("#test-button").first();
|
|
567
610
|
button.simulate("click", {
|
|
568
611
|
preventDefault() {
|
|
612
|
+
// $FlowIgnore[object-this-reference]
|
|
569
613
|
this.defaultPrevented = true;
|
|
570
614
|
},
|
|
571
615
|
});
|
|
@@ -1052,6 +1096,7 @@ describe("ClickableBehavior", () => {
|
|
|
1052
1096
|
const button = wrapper.find("#test-button").first();
|
|
1053
1097
|
button.simulate("click", {
|
|
1054
1098
|
preventDefault() {
|
|
1099
|
+
// $FlowIgnore[object-this-reference]
|
|
1055
1100
|
this.defaultPrevented = true;
|
|
1056
1101
|
},
|
|
1057
1102
|
});
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
import {MemoryRouter, Route, Switch} from "react-router-dom";
|
|
4
4
|
import {mount} from "enzyme";
|
|
5
|
+
import "jest-enzyme";
|
|
5
6
|
|
|
6
7
|
import {View} from "@khanacademy/wonder-blocks-core";
|
|
7
8
|
import Clickable from "../clickable.js";
|
|
@@ -13,6 +14,11 @@ const wait = (delay: number = 0) =>
|
|
|
13
14
|
});
|
|
14
15
|
|
|
15
16
|
describe("Clickable", () => {
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
delete window.location;
|
|
19
|
+
window.location = {assign: jest.fn()};
|
|
20
|
+
});
|
|
21
|
+
|
|
16
22
|
test("client-side navigation", () => {
|
|
17
23
|
// Arrange
|
|
18
24
|
const wrapper = mount(
|
|
@@ -129,8 +135,6 @@ describe("Clickable", () => {
|
|
|
129
135
|
|
|
130
136
|
test("should navigate to a specific link using the keyboard", () => {
|
|
131
137
|
// Arrange
|
|
132
|
-
window.location.assign = jest.fn();
|
|
133
|
-
|
|
134
138
|
const wrapper = mount(
|
|
135
139
|
<Clickable testId="button" href="/foo" skipClientNav={true}>
|
|
136
140
|
{(eventState) => <h1>Click Me!</h1>}
|
|
@@ -276,7 +280,6 @@ describe("Clickable", () => {
|
|
|
276
280
|
|
|
277
281
|
test("safeWithNav with skipClientNav=true waits for promise resolution", async () => {
|
|
278
282
|
// Arrange
|
|
279
|
-
jest.spyOn(window.location, "assign");
|
|
280
283
|
const wrapper = mount(
|
|
281
284
|
<MemoryRouter>
|
|
282
285
|
<div>
|
|
@@ -309,7 +312,6 @@ describe("Clickable", () => {
|
|
|
309
312
|
|
|
310
313
|
test("beforeNav resolution and safeWithNav with skipClientNav=true waits for promise resolution", async () => {
|
|
311
314
|
// Arrange
|
|
312
|
-
jest.spyOn(window.location, "assign");
|
|
313
315
|
const wrapper = mount(
|
|
314
316
|
<MemoryRouter>
|
|
315
317
|
<div>
|
|
@@ -336,8 +338,6 @@ describe("Clickable", () => {
|
|
|
336
338
|
buttonWrapper.simulate("click", {button: 0});
|
|
337
339
|
await wait(0);
|
|
338
340
|
buttonWrapper.update();
|
|
339
|
-
await wait(0);
|
|
340
|
-
buttonWrapper.update();
|
|
341
341
|
|
|
342
342
|
// Assert
|
|
343
343
|
expect(window.location.assign).toHaveBeenCalledWith("/foo");
|
|
@@ -345,7 +345,6 @@ describe("Clickable", () => {
|
|
|
345
345
|
|
|
346
346
|
test("safeWithNav with skipClientNav=true waits for promise rejection", async () => {
|
|
347
347
|
// Arrange
|
|
348
|
-
jest.spyOn(window.location, "assign");
|
|
349
348
|
const wrapper = mount(
|
|
350
349
|
<MemoryRouter>
|
|
351
350
|
<div>
|
|
@@ -376,9 +375,8 @@ describe("Clickable", () => {
|
|
|
376
375
|
expect(window.location.assign).toHaveBeenCalledWith("/foo");
|
|
377
376
|
});
|
|
378
377
|
|
|
379
|
-
test("safeWithNav with skipClientNav=false calls safeWithNav but doesn't wait to navigate",
|
|
378
|
+
test("safeWithNav with skipClientNav=false calls safeWithNav but doesn't wait to navigate", () => {
|
|
380
379
|
// Arrange
|
|
381
|
-
jest.spyOn(window.location, "assign");
|
|
382
380
|
const safeWithNavMock = jest.fn();
|
|
383
381
|
const wrapper = mount(
|
|
384
382
|
<MemoryRouter>
|
|
@@ -406,12 +404,16 @@ describe("Clickable", () => {
|
|
|
406
404
|
|
|
407
405
|
// Assert
|
|
408
406
|
expect(safeWithNavMock).toHaveBeenCalled();
|
|
409
|
-
expect(
|
|
407
|
+
expect(wrapper).toIncludeText(
|
|
408
|
+
"Hello, world!" /*client side nav to /foo*/,
|
|
409
|
+
);
|
|
410
|
+
expect(window.location.assign).not.toHaveBeenCalledWith(
|
|
411
|
+
"/foo" /*not a full page nav*/,
|
|
412
|
+
);
|
|
410
413
|
});
|
|
411
414
|
|
|
412
415
|
test("safeWithNav with beforeNav resolution and skipClientNav=false calls safeWithNav but doesn't wait to navigate", async () => {
|
|
413
416
|
// Arrange
|
|
414
|
-
jest.spyOn(window.location, "assign");
|
|
415
417
|
const safeWithNavMock = jest.fn();
|
|
416
418
|
const wrapper = mount(
|
|
417
419
|
<MemoryRouter>
|
|
@@ -442,7 +444,12 @@ describe("Clickable", () => {
|
|
|
442
444
|
|
|
443
445
|
// Assert
|
|
444
446
|
expect(safeWithNavMock).toHaveBeenCalled();
|
|
445
|
-
expect(
|
|
447
|
+
expect(wrapper).toIncludeText(
|
|
448
|
+
"Hello, world!" /*client side nav to /foo*/,
|
|
449
|
+
);
|
|
450
|
+
expect(window.location.assign).not.toHaveBeenCalledWith(
|
|
451
|
+
"/foo" /*not a full page nav*/,
|
|
452
|
+
);
|
|
446
453
|
});
|
|
447
454
|
|
|
448
455
|
describe("raw events", () => {
|
|
@@ -223,7 +223,9 @@ const disabledHandlers = {
|
|
|
223
223
|
onKeyUp: () => void 0,
|
|
224
224
|
onFocus: () => void 0,
|
|
225
225
|
onBlur: () => void 0,
|
|
226
|
-
|
|
226
|
+
// Clickable components should still be tabbable so they can
|
|
227
|
+
// be used as anchors.
|
|
228
|
+
tabIndex: 0,
|
|
227
229
|
};
|
|
228
230
|
|
|
229
231
|
const keyCodes = {
|
|
@@ -312,8 +314,8 @@ const startState: ClickableState = {
|
|
|
312
314
|
* The react-router aware version is returned if `router` is a react-router-dom
|
|
313
315
|
* router, `skipClientNav` is not `true`, and `href` is an internal URL.
|
|
314
316
|
*
|
|
315
|
-
* The `router` can be accessed via
|
|
316
|
-
* rendered as a descendant of a BrowserRouter.
|
|
317
|
+
* The `router` can be accessed via __RouterContext (imported from 'react-router')
|
|
318
|
+
* from a component rendered as a descendant of a BrowserRouter.
|
|
317
319
|
* See https://reacttraining.com/react-router/web/guides/basic-components.
|
|
318
320
|
*/
|
|
319
321
|
export default class ClickableBehavior extends React.Component<
|
|
@@ -558,9 +560,8 @@ export default class ClickableBehavior extends React.Component<
|
|
|
558
560
|
}
|
|
559
561
|
|
|
560
562
|
const keyCode = e.which || e.keyCode;
|
|
561
|
-
const {triggerOnEnter, triggerOnSpace} =
|
|
562
|
-
role
|
|
563
|
-
);
|
|
563
|
+
const {triggerOnEnter, triggerOnSpace} =
|
|
564
|
+
getAppropriateTriggersForRole(role);
|
|
564
565
|
if (
|
|
565
566
|
(triggerOnEnter && keyCode === keyCodes.enter) ||
|
|
566
567
|
(triggerOnSpace && keyCode === keyCodes.space)
|
|
@@ -585,9 +586,8 @@ export default class ClickableBehavior extends React.Component<
|
|
|
585
586
|
}
|
|
586
587
|
|
|
587
588
|
const keyCode = e.which || e.keyCode;
|
|
588
|
-
const {triggerOnEnter, triggerOnSpace} =
|
|
589
|
-
role
|
|
590
|
-
);
|
|
589
|
+
const {triggerOnEnter, triggerOnSpace} =
|
|
590
|
+
getAppropriateTriggersForRole(role);
|
|
591
591
|
if (
|
|
592
592
|
(triggerOnEnter && keyCode === keyCodes.enter) ||
|
|
593
593
|
(triggerOnSpace && keyCode === keyCodes.space)
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// @flow
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
import {StyleSheet} from "aphrodite";
|
|
4
|
-
import * as PropTypes from "prop-types";
|
|
5
4
|
import {Link} from "react-router-dom";
|
|
5
|
+
import {__RouterContext} from "react-router";
|
|
6
6
|
|
|
7
7
|
import {addStyle} from "@khanacademy/wonder-blocks-core";
|
|
8
8
|
import type {AriaProps, StyleType} from "@khanacademy/wonder-blocks-core";
|
|
@@ -169,10 +169,6 @@ type Props =
|
|
|
169
169
|
safeWithNav: () => Promise<mixed>,
|
|
170
170
|
|};
|
|
171
171
|
|
|
172
|
-
type ContextTypes = {|
|
|
173
|
-
router: $FlowFixMe,
|
|
174
|
-
|};
|
|
175
|
-
|
|
176
172
|
type DefaultProps = {|
|
|
177
173
|
light: $PropertyType<Props, "light">,
|
|
178
174
|
disabled: $PropertyType<Props, "disabled">,
|
|
@@ -208,8 +204,6 @@ const StyledLink = addStyle<typeof Link>(Link);
|
|
|
208
204
|
* ```
|
|
209
205
|
*/
|
|
210
206
|
export default class Clickable extends React.Component<Props> {
|
|
211
|
-
static contextTypes: ContextTypes = {router: PropTypes.any};
|
|
212
|
-
|
|
213
207
|
static defaultProps: DefaultProps = {
|
|
214
208
|
light: false,
|
|
215
209
|
disabled: false,
|
|
@@ -218,11 +212,12 @@ export default class Clickable extends React.Component<Props> {
|
|
|
218
212
|
|
|
219
213
|
getCorrectTag: (
|
|
220
214
|
clickableState: ClickableState,
|
|
215
|
+
router: any,
|
|
221
216
|
commonProps: {[string]: any, ...},
|
|
222
|
-
) => React.Node = (clickableState, commonProps) => {
|
|
217
|
+
) => React.Node = (clickableState, router, commonProps) => {
|
|
223
218
|
const activeHref = this.props.href && !this.props.disabled;
|
|
224
219
|
const useClient =
|
|
225
|
-
|
|
220
|
+
router &&
|
|
226
221
|
!this.props.skipClientNav &&
|
|
227
222
|
isClientSideUrl(this.props.href || "");
|
|
228
223
|
|
|
@@ -265,7 +260,7 @@ export default class Clickable extends React.Component<Props> {
|
|
|
265
260
|
}
|
|
266
261
|
};
|
|
267
262
|
|
|
268
|
-
|
|
263
|
+
renderClickableBehavior(router: any): React.Node {
|
|
269
264
|
const {
|
|
270
265
|
href,
|
|
271
266
|
onClick,
|
|
@@ -285,7 +280,7 @@ export default class Clickable extends React.Component<Props> {
|
|
|
285
280
|
const ClickableBehavior = getClickableBehavior(
|
|
286
281
|
href,
|
|
287
282
|
skipClientNav,
|
|
288
|
-
|
|
283
|
+
router,
|
|
289
284
|
);
|
|
290
285
|
|
|
291
286
|
const getStyle = (state: ClickableState): StyleType => [
|
|
@@ -309,7 +304,7 @@ export default class Clickable extends React.Component<Props> {
|
|
|
309
304
|
disabled={disabled}
|
|
310
305
|
>
|
|
311
306
|
{(state, childrenProps) =>
|
|
312
|
-
this.getCorrectTag(state, {
|
|
307
|
+
this.getCorrectTag(state, router, {
|
|
313
308
|
...restProps,
|
|
314
309
|
"data-test-id": testId,
|
|
315
310
|
style: getStyle(state),
|
|
@@ -330,7 +325,7 @@ export default class Clickable extends React.Component<Props> {
|
|
|
330
325
|
disabled={disabled}
|
|
331
326
|
>
|
|
332
327
|
{(state, childrenProps) =>
|
|
333
|
-
this.getCorrectTag(state, {
|
|
328
|
+
this.getCorrectTag(state, router, {
|
|
334
329
|
...restProps,
|
|
335
330
|
"data-test-id": testId,
|
|
336
331
|
style: getStyle(state),
|
|
@@ -341,6 +336,14 @@ export default class Clickable extends React.Component<Props> {
|
|
|
341
336
|
);
|
|
342
337
|
}
|
|
343
338
|
}
|
|
339
|
+
|
|
340
|
+
render(): React.Node {
|
|
341
|
+
return (
|
|
342
|
+
<__RouterContext.Consumer>
|
|
343
|
+
{(router) => this.renderClickableBehavior(router)}
|
|
344
|
+
</__RouterContext.Consumer>
|
|
345
|
+
);
|
|
346
|
+
}
|
|
344
347
|
}
|
|
345
348
|
|
|
346
349
|
// Source: https://gist.github.com/MoOx/9137295
|
|
@@ -145,6 +145,13 @@ const styles = StyleSheet.create({
|
|
|
145
145
|
</MemoryRouter>
|
|
146
146
|
```
|
|
147
147
|
|
|
148
|
+
### Running callbacks on navigation
|
|
149
|
+
|
|
150
|
+
When using the `href` prop, the `onClick`, `beforeNav`, and `safeWithNav` props
|
|
151
|
+
can be used to run callbacks when navigating to the new URL. Which prop to use
|
|
152
|
+
depends on the use case. See the [Button](#section-button) documentation for
|
|
153
|
+
details.
|
|
154
|
+
|
|
148
155
|
### Navigating with the Keyboard
|
|
149
156
|
|
|
150
157
|
Clickable adds support to keyboard navigation. This way, your components are
|
|
@@ -12,10 +12,10 @@ import {Body} from "@khanacademy/wonder-blocks-typography";
|
|
|
12
12
|
import type {StoryComponentType} from "@storybook/react";
|
|
13
13
|
|
|
14
14
|
export default {
|
|
15
|
-
title: "Clickable",
|
|
15
|
+
title: "Navigation/Clickable",
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
-
export const
|
|
18
|
+
export const Basic: StoryComponentType = () => (
|
|
19
19
|
<View>
|
|
20
20
|
<View style={styles.centerText}>
|
|
21
21
|
<Clickable
|
|
@@ -56,7 +56,7 @@ export const basic: StoryComponentType = () => (
|
|
|
56
56
|
</View>
|
|
57
57
|
);
|
|
58
58
|
|
|
59
|
-
export const
|
|
59
|
+
export const KeyboardNavigation: StoryComponentType = () => (
|
|
60
60
|
<View>
|
|
61
61
|
<Clickable
|
|
62
62
|
href="https://www.khanacademy.org/about/tos"
|
|
@@ -77,16 +77,14 @@ export const keyboardNavigation: StoryComponentType = () => (
|
|
|
77
77
|
</View>
|
|
78
78
|
);
|
|
79
79
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
disable: true,
|
|
85
|
-
},
|
|
80
|
+
KeyboardNavigation.parameters = {
|
|
81
|
+
chromatic: {
|
|
82
|
+
// we don't need screenshots because this story only tests behavior.
|
|
83
|
+
disableSnapshot: true,
|
|
86
84
|
},
|
|
87
85
|
};
|
|
88
86
|
|
|
89
|
-
export const
|
|
87
|
+
export const KeyboardNavigationTab: StoryComponentType = () => (
|
|
90
88
|
<View>
|
|
91
89
|
<Clickable role="tab" aria-controls="panel-1" id="tab-1">
|
|
92
90
|
{({hovered, focused, pressed}) => (
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* The react-router aware version is returned if `router` is a react-router-dom
|
|
6
6
|
* router, `skipClientNav` is not `true`, and `href` is an internal URL.
|
|
7
7
|
*
|
|
8
|
-
* The `router` can be accessed via
|
|
9
|
-
* as a descendant of a BrowserRouter.
|
|
8
|
+
* The `router` can be accessed via __RouterContext (imported from 'react-router')
|
|
9
|
+
* from a component rendered as a descendant of a BrowserRouter.
|
|
10
10
|
* See https://reacttraining.com/react-router/web/guides/basic-components.
|
|
11
11
|
*/
|
|
12
12
|
import * as React from "react";
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2018 Khan Academy
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|