@khanacademy/wonder-blocks-clickable 2.2.0 → 2.2.4
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 +19 -0
- package/dist/es/index.js +24 -18
- package/dist/index.js +446 -147
- package/package.json +5 -6
- package/src/__tests__/__snapshots__/generated-snapshot.test.js.snap +2 -2
- package/src/components/__tests__/clickable-behavior.test.js +49 -4
- package/src/components/__tests__/clickable.test.js +62 -12
- package/src/components/clickable-behavior.js +16 -12
- package/src/components/clickable.js +1 -1
- package/src/components/clickable.md +7 -0
- package/src/components/clickable.stories.js +4 -4
- package/LICENSE +0 -21
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# @khanacademy/wonder-blocks-clickable
|
|
2
|
+
|
|
3
|
+
## 2.2.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 166ecc97: Use `aria-disabled` instead of disabled, fix focused + disabled styles.
|
|
8
|
+
|
|
9
|
+
## 2.2.3
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- @khanacademy/wonder-blocks-core@4.2.1
|
|
14
|
+
|
|
15
|
+
## 2.2.2
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- 901bfe82: Change disabled tabindex from -1 to 0
|
package/dist/es/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import _objectWithoutPropertiesLoose from '@babel/runtime/helpers/objectWithoutPropertiesLoose';
|
|
2
2
|
import _extends from '@babel/runtime/helpers/extends';
|
|
3
|
-
import
|
|
3
|
+
import * as React from 'react';
|
|
4
4
|
import { StyleSheet } from 'aphrodite';
|
|
5
5
|
import { withRouter, Link } from 'react-router-dom';
|
|
6
6
|
import { __RouterContext } from 'react-router';
|
|
@@ -50,9 +50,9 @@ const disabledHandlers = {
|
|
|
50
50
|
onTouchCancel: () => void 0,
|
|
51
51
|
onKeyDown: () => void 0,
|
|
52
52
|
onKeyUp: () => void 0,
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
tabIndex:
|
|
53
|
+
// Clickable components should still be tabbable so they can
|
|
54
|
+
// be used as anchors.
|
|
55
|
+
tabIndex: 0
|
|
56
56
|
};
|
|
57
57
|
const keyCodes = {
|
|
58
58
|
enter: 13,
|
|
@@ -143,11 +143,14 @@ const startState = {
|
|
|
143
143
|
* See https://reacttraining.com/react-router/web/guides/basic-components.
|
|
144
144
|
*/
|
|
145
145
|
|
|
146
|
-
class ClickableBehavior extends Component {
|
|
146
|
+
class ClickableBehavior extends React.Component {
|
|
147
147
|
static getDerivedStateFromProps(props, state) {
|
|
148
|
-
// If new props are disabled, reset the hovered/
|
|
148
|
+
// If new props are disabled, reset the hovered/pressed states
|
|
149
149
|
if (props.disabled) {
|
|
150
|
-
|
|
150
|
+
// Keep the focused state for enabling keyboard navigation.
|
|
151
|
+
return _extends({}, startState, {
|
|
152
|
+
focused: state.focused
|
|
153
|
+
});
|
|
151
154
|
} else {
|
|
152
155
|
// Cannot return undefined
|
|
153
156
|
return null;
|
|
@@ -461,7 +464,11 @@ class ClickableBehavior extends Component {
|
|
|
461
464
|
}
|
|
462
465
|
|
|
463
466
|
render() {
|
|
464
|
-
const childrenProps = this.props.disabled ? disabledHandlers
|
|
467
|
+
const childrenProps = this.props.disabled ? _extends({}, disabledHandlers, {
|
|
468
|
+
// Keep these handlers for keyboard accessibility.
|
|
469
|
+
onFocus: this.handleFocus,
|
|
470
|
+
onBlur: this.handleBlur
|
|
471
|
+
}) : {
|
|
465
472
|
onClick: this.handleClick,
|
|
466
473
|
onMouseEnter: this.handleMouseEnter,
|
|
467
474
|
onMouseLeave: this.handleMouseLeave,
|
|
@@ -574,7 +581,7 @@ const StyledLink = addStyle(Link);
|
|
|
574
581
|
* ```
|
|
575
582
|
*/
|
|
576
583
|
|
|
577
|
-
class Clickable extends Component {
|
|
584
|
+
class Clickable extends React.Component {
|
|
578
585
|
constructor(...args) {
|
|
579
586
|
super(...args);
|
|
580
587
|
|
|
@@ -584,23 +591,23 @@ class Clickable extends Component {
|
|
|
584
591
|
// needs it to refine this.props.href to a string.
|
|
585
592
|
|
|
586
593
|
if (activeHref && useClient && this.props.href) {
|
|
587
|
-
return /*#__PURE__*/createElement(StyledLink, _extends({}, commonProps, {
|
|
594
|
+
return /*#__PURE__*/React.createElement(StyledLink, _extends({}, commonProps, {
|
|
588
595
|
to: this.props.href,
|
|
589
596
|
role: this.props.role,
|
|
590
597
|
target: this.props.target || undefined,
|
|
591
598
|
"aria-disabled": this.props.disabled ? "true" : undefined
|
|
592
599
|
}), this.props.children(clickableState));
|
|
593
600
|
} else if (activeHref && !useClient) {
|
|
594
|
-
return /*#__PURE__*/createElement(StyledAnchor, _extends({}, commonProps, {
|
|
601
|
+
return /*#__PURE__*/React.createElement(StyledAnchor, _extends({}, commonProps, {
|
|
595
602
|
href: this.props.href,
|
|
596
603
|
role: this.props.role,
|
|
597
604
|
target: this.props.target || undefined,
|
|
598
605
|
"aria-disabled": this.props.disabled ? "true" : undefined
|
|
599
606
|
}), this.props.children(clickableState));
|
|
600
607
|
} else {
|
|
601
|
-
return /*#__PURE__*/createElement(StyledButton, _extends({}, commonProps, {
|
|
608
|
+
return /*#__PURE__*/React.createElement(StyledButton, _extends({}, commonProps, {
|
|
602
609
|
type: "button",
|
|
603
|
-
disabled: this.props.disabled
|
|
610
|
+
"aria-disabled": this.props.disabled
|
|
604
611
|
}), this.props.children(clickableState));
|
|
605
612
|
}
|
|
606
613
|
};
|
|
@@ -630,7 +637,7 @@ class Clickable extends Component {
|
|
|
630
637
|
const getStyle = state => [styles.reset, styles.link, !hideDefaultFocusRing && state.focused && (light ? styles.focusedLight : styles.focused), style];
|
|
631
638
|
|
|
632
639
|
if (beforeNav) {
|
|
633
|
-
return /*#__PURE__*/createElement(ClickableBehavior, {
|
|
640
|
+
return /*#__PURE__*/React.createElement(ClickableBehavior, {
|
|
634
641
|
href: href,
|
|
635
642
|
onClick: onClick,
|
|
636
643
|
beforeNav: beforeNav,
|
|
@@ -643,7 +650,7 @@ class Clickable extends Component {
|
|
|
643
650
|
style: getStyle(state)
|
|
644
651
|
}, childrenProps)));
|
|
645
652
|
} else {
|
|
646
|
-
return /*#__PURE__*/createElement(ClickableBehavior, {
|
|
653
|
+
return /*#__PURE__*/React.createElement(ClickableBehavior, {
|
|
647
654
|
href: href,
|
|
648
655
|
onClick: onClick,
|
|
649
656
|
safeWithNav: safeWithNav,
|
|
@@ -659,7 +666,7 @@ class Clickable extends Component {
|
|
|
659
666
|
}
|
|
660
667
|
|
|
661
668
|
render() {
|
|
662
|
-
return /*#__PURE__*/createElement(__RouterContext.Consumer, null, router => this.renderClickableBehavior(router));
|
|
669
|
+
return /*#__PURE__*/React.createElement(__RouterContext.Consumer, null, router => this.renderClickableBehavior(router));
|
|
663
670
|
}
|
|
664
671
|
|
|
665
672
|
} // Source: https://gist.github.com/MoOx/9137295
|
|
@@ -709,5 +716,4 @@ const styles = StyleSheet.create({
|
|
|
709
716
|
}
|
|
710
717
|
});
|
|
711
718
|
|
|
712
|
-
export default
|
|
713
|
-
export { ClickableBehavior, getClickableBehavior, isClientSideUrl };
|
|
719
|
+
export { ClickableBehavior, Clickable as default, getClickableBehavior, isClientSideUrl };
|
package/dist/index.js
CHANGED
|
@@ -82,7 +82,7 @@ module.exports =
|
|
|
82
82
|
/******/
|
|
83
83
|
/******/
|
|
84
84
|
/******/ // Load entry module and return exports
|
|
85
|
-
/******/ return __webpack_require__(__webpack_require__.s =
|
|
85
|
+
/******/ return __webpack_require__(__webpack_require__.s = 10);
|
|
86
86
|
/******/ })
|
|
87
87
|
/************************************************************************/
|
|
88
88
|
/******/ ([
|
|
@@ -164,9 +164,9 @@ const disabledHandlers = {
|
|
|
164
164
|
onTouchCancel: () => void 0,
|
|
165
165
|
onKeyDown: () => void 0,
|
|
166
166
|
onKeyUp: () => void 0,
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
tabIndex:
|
|
167
|
+
// Clickable components should still be tabbable so they can
|
|
168
|
+
// be used as anchors.
|
|
169
|
+
tabIndex: 0
|
|
170
170
|
};
|
|
171
171
|
const keyCodes = {
|
|
172
172
|
enter: 13,
|
|
@@ -259,9 +259,12 @@ const startState = {
|
|
|
259
259
|
|
|
260
260
|
class ClickableBehavior extends react__WEBPACK_IMPORTED_MODULE_0__["Component"] {
|
|
261
261
|
static getDerivedStateFromProps(props, state) {
|
|
262
|
-
// If new props are disabled, reset the hovered/
|
|
262
|
+
// If new props are disabled, reset the hovered/pressed states
|
|
263
263
|
if (props.disabled) {
|
|
264
|
-
|
|
264
|
+
// Keep the focused state for enabling keyboard navigation.
|
|
265
|
+
return { ...startState,
|
|
266
|
+
focused: state.focused
|
|
267
|
+
};
|
|
265
268
|
} else {
|
|
266
269
|
// Cannot return undefined
|
|
267
270
|
return null;
|
|
@@ -575,7 +578,11 @@ class ClickableBehavior extends react__WEBPACK_IMPORTED_MODULE_0__["Component"]
|
|
|
575
578
|
}
|
|
576
579
|
|
|
577
580
|
render() {
|
|
578
|
-
const childrenProps = this.props.disabled ? disabledHandlers
|
|
581
|
+
const childrenProps = this.props.disabled ? { ...disabledHandlers,
|
|
582
|
+
// Keep these handlers for keyboard accessibility.
|
|
583
|
+
onFocus: this.handleFocus,
|
|
584
|
+
onBlur: this.handleBlur
|
|
585
|
+
} : {
|
|
579
586
|
onClick: this.handleClick,
|
|
580
587
|
onMouseEnter: this.handleMouseEnter,
|
|
581
588
|
onMouseLeave: this.handleMouseLeave,
|
|
@@ -675,142 +682,459 @@ module.exports = require("@khanacademy/wonder-blocks-core");
|
|
|
675
682
|
|
|
676
683
|
/***/ }),
|
|
677
684
|
/* 6 */
|
|
678
|
-
/***/ (function(module,
|
|
685
|
+
/***/ (function(module, exports) {
|
|
679
686
|
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
/* harmony import */ var _babel_runtime_helpers_extends__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(10);
|
|
685
|
-
/* harmony import */ var _babel_runtime_helpers_extends__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_babel_runtime_helpers_extends__WEBPACK_IMPORTED_MODULE_0__);
|
|
687
|
+
module.exports =
|
|
688
|
+
/******/
|
|
689
|
+
function (modules) {
|
|
690
|
+
// webpackBootstrap
|
|
686
691
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
* mixing colors together.
|
|
690
|
-
*/
|
|
692
|
+
/******/
|
|
693
|
+
// The module cache
|
|
691
694
|
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
// with r,g,b,a keys.
|
|
695
|
+
/******/
|
|
696
|
+
var installedModules = {};
|
|
697
|
+
/******/
|
|
696
698
|
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
throw new Error(`Failed to parse color: ${color}`);
|
|
700
|
-
}
|
|
699
|
+
/******/
|
|
700
|
+
// The require function
|
|
701
701
|
|
|
702
|
-
|
|
702
|
+
/******/
|
|
703
703
|
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
r: parseInt(`${color3Match[1]}${color3Match[1]}`, 16),
|
|
707
|
-
g: parseInt(`${color3Match[2]}${color3Match[2]}`, 16),
|
|
708
|
-
b: parseInt(`${color3Match[3]}${color3Match[3]}`, 16),
|
|
709
|
-
a: 1
|
|
710
|
-
};
|
|
711
|
-
}
|
|
704
|
+
function __webpack_require__(moduleId) {
|
|
705
|
+
/******/
|
|
712
706
|
|
|
713
|
-
|
|
707
|
+
/******/
|
|
708
|
+
// Check if module is in cache
|
|
714
709
|
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
710
|
+
/******/
|
|
711
|
+
if (installedModules[moduleId]) {
|
|
712
|
+
/******/
|
|
713
|
+
return installedModules[moduleId].exports;
|
|
714
|
+
/******/
|
|
715
|
+
}
|
|
716
|
+
/******/
|
|
717
|
+
// Create a new module (and put it into the cache)
|
|
723
718
|
|
|
724
|
-
|
|
719
|
+
/******/
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
var module = installedModules[moduleId] = {
|
|
723
|
+
/******/
|
|
724
|
+
i: moduleId,
|
|
725
|
+
|
|
726
|
+
/******/
|
|
727
|
+
l: false,
|
|
728
|
+
|
|
729
|
+
/******/
|
|
730
|
+
exports: {}
|
|
731
|
+
/******/
|
|
725
732
|
|
|
726
|
-
if (rgbaMatch) {
|
|
727
|
-
return {
|
|
728
|
-
r: parseFloat(rgbaMatch[1]),
|
|
729
|
-
g: parseFloat(rgbaMatch[2]),
|
|
730
|
-
b: parseFloat(rgbaMatch[3]),
|
|
731
|
-
a: rgbaMatch[4] ? parseFloat(rgbaMatch[4]) : 1
|
|
732
733
|
};
|
|
734
|
+
/******/
|
|
735
|
+
|
|
736
|
+
/******/
|
|
737
|
+
// Execute the module function
|
|
738
|
+
|
|
739
|
+
/******/
|
|
740
|
+
|
|
741
|
+
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
|
|
742
|
+
/******/
|
|
743
|
+
|
|
744
|
+
/******/
|
|
745
|
+
// Flag the module as loaded
|
|
746
|
+
|
|
747
|
+
/******/
|
|
748
|
+
|
|
749
|
+
module.l = true;
|
|
750
|
+
/******/
|
|
751
|
+
|
|
752
|
+
/******/
|
|
753
|
+
// Return the exports of the module
|
|
754
|
+
|
|
755
|
+
/******/
|
|
756
|
+
|
|
757
|
+
return module.exports;
|
|
758
|
+
/******/
|
|
733
759
|
}
|
|
760
|
+
/******/
|
|
761
|
+
|
|
762
|
+
/******/
|
|
763
|
+
|
|
764
|
+
/******/
|
|
765
|
+
// expose the modules object (__webpack_modules__)
|
|
766
|
+
|
|
767
|
+
/******/
|
|
734
768
|
|
|
735
|
-
throw new Error(`Failed to parse color: ${color}`);
|
|
736
|
-
}; // Stringify the color in an `rgba()` or `#abcdef` format.
|
|
737
769
|
|
|
770
|
+
__webpack_require__.m = modules;
|
|
771
|
+
/******/
|
|
772
|
+
|
|
773
|
+
/******/
|
|
774
|
+
// expose the module cache
|
|
775
|
+
|
|
776
|
+
/******/
|
|
777
|
+
|
|
778
|
+
__webpack_require__.c = installedModules;
|
|
779
|
+
/******/
|
|
780
|
+
|
|
781
|
+
/******/
|
|
782
|
+
// define getter function for harmony exports
|
|
783
|
+
|
|
784
|
+
/******/
|
|
785
|
+
|
|
786
|
+
__webpack_require__.d = function (exports, name, getter) {
|
|
787
|
+
/******/
|
|
788
|
+
if (!__webpack_require__.o(exports, name)) {
|
|
789
|
+
/******/
|
|
790
|
+
Object.defineProperty(exports, name, {
|
|
791
|
+
enumerable: true,
|
|
792
|
+
get: getter
|
|
793
|
+
});
|
|
794
|
+
/******/
|
|
795
|
+
}
|
|
796
|
+
/******/
|
|
797
|
+
|
|
798
|
+
};
|
|
799
|
+
/******/
|
|
738
800
|
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
const g = Math.round(color.g);
|
|
742
|
-
const b = Math.round(color.b);
|
|
801
|
+
/******/
|
|
802
|
+
// define __esModule on exports
|
|
743
803
|
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
804
|
+
/******/
|
|
805
|
+
|
|
806
|
+
|
|
807
|
+
__webpack_require__.r = function (exports) {
|
|
808
|
+
/******/
|
|
809
|
+
if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
|
|
810
|
+
/******/
|
|
811
|
+
Object.defineProperty(exports, Symbol.toStringTag, {
|
|
812
|
+
value: 'Module'
|
|
813
|
+
});
|
|
814
|
+
/******/
|
|
815
|
+
}
|
|
816
|
+
/******/
|
|
817
|
+
|
|
818
|
+
|
|
819
|
+
Object.defineProperty(exports, '__esModule', {
|
|
820
|
+
value: true
|
|
821
|
+
});
|
|
822
|
+
/******/
|
|
823
|
+
};
|
|
824
|
+
/******/
|
|
825
|
+
|
|
826
|
+
/******/
|
|
827
|
+
// create a fake namespace object
|
|
828
|
+
|
|
829
|
+
/******/
|
|
830
|
+
// mode & 1: value is a module id, require it
|
|
831
|
+
|
|
832
|
+
/******/
|
|
833
|
+
// mode & 2: merge all properties of value into the ns
|
|
834
|
+
|
|
835
|
+
/******/
|
|
836
|
+
// mode & 4: return value when already ns object
|
|
837
|
+
|
|
838
|
+
/******/
|
|
839
|
+
// mode & 8|1: behave like require
|
|
840
|
+
|
|
841
|
+
/******/
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
__webpack_require__.t = function (value, mode) {
|
|
845
|
+
/******/
|
|
846
|
+
if (mode & 1) value = __webpack_require__(value);
|
|
847
|
+
/******/
|
|
848
|
+
|
|
849
|
+
if (mode & 8) return value;
|
|
850
|
+
/******/
|
|
851
|
+
|
|
852
|
+
if (mode & 4 && typeof value === 'object' && value && value.__esModule) return value;
|
|
853
|
+
/******/
|
|
854
|
+
|
|
855
|
+
var ns = Object.create(null);
|
|
856
|
+
/******/
|
|
857
|
+
|
|
858
|
+
__webpack_require__.r(ns);
|
|
859
|
+
/******/
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
Object.defineProperty(ns, 'default', {
|
|
863
|
+
enumerable: true,
|
|
864
|
+
value: value
|
|
865
|
+
});
|
|
866
|
+
/******/
|
|
867
|
+
|
|
868
|
+
if (mode & 2 && typeof value != 'string') for (var key in value) __webpack_require__.d(ns, key, function (key) {
|
|
869
|
+
return value[key];
|
|
870
|
+
}.bind(null, key));
|
|
871
|
+
/******/
|
|
872
|
+
|
|
873
|
+
return ns;
|
|
874
|
+
/******/
|
|
875
|
+
};
|
|
876
|
+
/******/
|
|
877
|
+
|
|
878
|
+
/******/
|
|
879
|
+
// getDefaultExport function for compatibility with non-harmony modules
|
|
880
|
+
|
|
881
|
+
/******/
|
|
882
|
+
|
|
883
|
+
|
|
884
|
+
__webpack_require__.n = function (module) {
|
|
885
|
+
/******/
|
|
886
|
+
var getter = module && module.__esModule ?
|
|
887
|
+
/******/
|
|
888
|
+
function getDefault() {
|
|
889
|
+
return module['default'];
|
|
890
|
+
} :
|
|
891
|
+
/******/
|
|
892
|
+
function getModuleExports() {
|
|
893
|
+
return module;
|
|
748
894
|
};
|
|
895
|
+
/******/
|
|
749
896
|
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
return `rgba(${r},${g},${b},${color.a.toFixed(2)})`;
|
|
753
|
-
}
|
|
754
|
-
}; // Adjust the alpha value of a color.
|
|
897
|
+
__webpack_require__.d(getter, 'a', getter);
|
|
898
|
+
/******/
|
|
755
899
|
|
|
756
900
|
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
901
|
+
return getter;
|
|
902
|
+
/******/
|
|
903
|
+
};
|
|
904
|
+
/******/
|
|
905
|
+
|
|
906
|
+
/******/
|
|
907
|
+
// Object.prototype.hasOwnProperty.call
|
|
908
|
+
|
|
909
|
+
/******/
|
|
910
|
+
|
|
911
|
+
|
|
912
|
+
__webpack_require__.o = function (object, property) {
|
|
913
|
+
return Object.prototype.hasOwnProperty.call(object, property);
|
|
914
|
+
};
|
|
915
|
+
/******/
|
|
916
|
+
|
|
917
|
+
/******/
|
|
918
|
+
// __webpack_public_path__
|
|
919
|
+
|
|
920
|
+
/******/
|
|
921
|
+
|
|
922
|
+
|
|
923
|
+
__webpack_require__.p = "";
|
|
924
|
+
/******/
|
|
925
|
+
|
|
926
|
+
/******/
|
|
761
927
|
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
928
|
+
/******/
|
|
929
|
+
// Load entry module and return exports
|
|
930
|
+
|
|
931
|
+
/******/
|
|
932
|
+
|
|
933
|
+
return __webpack_require__(__webpack_require__.s = 1);
|
|
934
|
+
/******/
|
|
935
|
+
}
|
|
936
|
+
/************************************************************************/
|
|
937
|
+
|
|
938
|
+
/******/
|
|
939
|
+
([
|
|
940
|
+
/* 0 */
|
|
941
|
+
|
|
942
|
+
/***/
|
|
943
|
+
function (module, __webpack_exports__, __webpack_require__) {
|
|
944
|
+
"use strict";
|
|
945
|
+
/* harmony export (binding) */
|
|
946
|
+
|
|
947
|
+
__webpack_require__.d(__webpack_exports__, "a", function () {
|
|
948
|
+
return fade;
|
|
778
949
|
});
|
|
779
|
-
|
|
950
|
+
/* harmony export (binding) */
|
|
780
951
|
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
}
|
|
811
|
-
|
|
952
|
+
|
|
953
|
+
__webpack_require__.d(__webpack_exports__, "b", function () {
|
|
954
|
+
return mix;
|
|
955
|
+
});
|
|
956
|
+
/**
|
|
957
|
+
* A color manipulation library useful for dynamically
|
|
958
|
+
* mixing colors together.
|
|
959
|
+
*/
|
|
960
|
+
|
|
961
|
+
|
|
962
|
+
const color6Regexp = /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i;
|
|
963
|
+
const color3Regexp = /^#([0-9a-f])([0-9a-f])([0-9a-f])$/i;
|
|
964
|
+
const rgbaRegexp = /^rgba?\(\s*(\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\s*\)$/i; // Parse a color in #abcdef, rgb(...), or rgba(...) form into an object
|
|
965
|
+
// with r,g,b,a keys.
|
|
966
|
+
|
|
967
|
+
const parse = color => {
|
|
968
|
+
if (typeof color !== "string") {
|
|
969
|
+
throw new Error(`Failed to parse color: ${color}`);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
const color3Match = color.match(color3Regexp);
|
|
973
|
+
|
|
974
|
+
if (color3Match) {
|
|
975
|
+
return {
|
|
976
|
+
r: parseInt(`${color3Match[1]}${color3Match[1]}`, 16),
|
|
977
|
+
g: parseInt(`${color3Match[2]}${color3Match[2]}`, 16),
|
|
978
|
+
b: parseInt(`${color3Match[3]}${color3Match[3]}`, 16),
|
|
979
|
+
a: 1
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
const color6Match = color.match(color6Regexp);
|
|
984
|
+
|
|
985
|
+
if (color6Match) {
|
|
986
|
+
return {
|
|
987
|
+
r: parseInt(color6Match[1], 16),
|
|
988
|
+
g: parseInt(color6Match[2], 16),
|
|
989
|
+
b: parseInt(color6Match[3], 16),
|
|
990
|
+
a: 1
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
const rgbaMatch = color.match(rgbaRegexp);
|
|
995
|
+
|
|
996
|
+
if (rgbaMatch) {
|
|
997
|
+
return {
|
|
998
|
+
r: parseFloat(rgbaMatch[1]),
|
|
999
|
+
g: parseFloat(rgbaMatch[2]),
|
|
1000
|
+
b: parseFloat(rgbaMatch[3]),
|
|
1001
|
+
a: rgbaMatch[4] ? parseFloat(rgbaMatch[4]) : 1
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
throw new Error(`Failed to parse color: ${color}`);
|
|
1006
|
+
}; // Stringify the color in an `rgba()` or `#abcdef` format.
|
|
1007
|
+
|
|
1008
|
+
|
|
1009
|
+
const format = color => {
|
|
1010
|
+
const r = Math.round(color.r);
|
|
1011
|
+
const g = Math.round(color.g);
|
|
1012
|
+
const b = Math.round(color.b);
|
|
1013
|
+
|
|
1014
|
+
if (color.a === 1) {
|
|
1015
|
+
const _s = c => {
|
|
1016
|
+
const asString = c.toString(16);
|
|
1017
|
+
return asString.length === 1 ? asString + asString : asString;
|
|
1018
|
+
};
|
|
1019
|
+
|
|
1020
|
+
return `#${_s(r)}${_s(g)}${_s(b)}`;
|
|
1021
|
+
} else {
|
|
1022
|
+
return `rgba(${r},${g},${b},${color.a.toFixed(2)})`;
|
|
1023
|
+
}
|
|
1024
|
+
}; // Adjust the alpha value of a color.
|
|
1025
|
+
|
|
1026
|
+
|
|
1027
|
+
const fade = (color, percentage) => {
|
|
1028
|
+
if (percentage < 0 || percentage > 1) {
|
|
1029
|
+
throw new Error("Percentage must be between 0 and 1");
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
const components = parse(color);
|
|
1033
|
+
return format({ ...components,
|
|
1034
|
+
a: components.a * percentage
|
|
1035
|
+
});
|
|
1036
|
+
}; // Mix a color into a background color, using the alpha channel of the base
|
|
1037
|
+
// color to determine the linear blend.
|
|
1038
|
+
|
|
1039
|
+
|
|
1040
|
+
const mix = (color, background) => {
|
|
1041
|
+
const colorObj = parse(color);
|
|
1042
|
+
const bgObj = parse(background);
|
|
1043
|
+
return format({
|
|
1044
|
+
r: colorObj.r * colorObj.a + bgObj.r * (1 - colorObj.a),
|
|
1045
|
+
g: colorObj.g * colorObj.a + bgObj.g * (1 - colorObj.a),
|
|
1046
|
+
b: colorObj.b * colorObj.a + bgObj.b * (1 - colorObj.a),
|
|
1047
|
+
a: bgObj.a
|
|
1048
|
+
});
|
|
1049
|
+
};
|
|
1050
|
+
/***/
|
|
1051
|
+
|
|
1052
|
+
},
|
|
1053
|
+
/* 1 */
|
|
1054
|
+
|
|
1055
|
+
/***/
|
|
1056
|
+
function (module, __webpack_exports__, __webpack_require__) {
|
|
1057
|
+
"use strict";
|
|
1058
|
+
|
|
1059
|
+
__webpack_require__.r(__webpack_exports__);
|
|
1060
|
+
/* harmony export (binding) */
|
|
1061
|
+
|
|
1062
|
+
|
|
1063
|
+
__webpack_require__.d(__webpack_exports__, "default", function () {
|
|
1064
|
+
return Color;
|
|
1065
|
+
});
|
|
1066
|
+
/* harmony export (binding) */
|
|
1067
|
+
|
|
1068
|
+
|
|
1069
|
+
__webpack_require__.d(__webpack_exports__, "SemanticColor", function () {
|
|
1070
|
+
return SemanticColor;
|
|
1071
|
+
});
|
|
1072
|
+
/* harmony import */
|
|
1073
|
+
|
|
1074
|
+
|
|
1075
|
+
var _util_utils_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(0);
|
|
1076
|
+
/* harmony reexport (safe) */
|
|
1077
|
+
|
|
1078
|
+
|
|
1079
|
+
__webpack_require__.d(__webpack_exports__, "mix", function () {
|
|
1080
|
+
return _util_utils_js__WEBPACK_IMPORTED_MODULE_0__["b"];
|
|
1081
|
+
});
|
|
1082
|
+
/* harmony reexport (safe) */
|
|
812
1083
|
|
|
813
1084
|
|
|
1085
|
+
__webpack_require__.d(__webpack_exports__, "fade", function () {
|
|
1086
|
+
return _util_utils_js__WEBPACK_IMPORTED_MODULE_0__["a"];
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
const offBlack = "#21242c";
|
|
1090
|
+
const white = "#ffffff";
|
|
1091
|
+
const Color = {
|
|
1092
|
+
// Product
|
|
1093
|
+
blue: "#1865f2",
|
|
1094
|
+
purple: "#9059ff",
|
|
1095
|
+
green: "#00a60e",
|
|
1096
|
+
gold: "#ffb100",
|
|
1097
|
+
red: "#d92916",
|
|
1098
|
+
// Neutral
|
|
1099
|
+
offBlack,
|
|
1100
|
+
offBlack64: Object(_util_utils_js__WEBPACK_IMPORTED_MODULE_0__[
|
|
1101
|
+
/* fade */
|
|
1102
|
+
"a"])(offBlack, 0.64),
|
|
1103
|
+
offBlack50: Object(_util_utils_js__WEBPACK_IMPORTED_MODULE_0__[
|
|
1104
|
+
/* fade */
|
|
1105
|
+
"a"])(offBlack, 0.5),
|
|
1106
|
+
offBlack32: Object(_util_utils_js__WEBPACK_IMPORTED_MODULE_0__[
|
|
1107
|
+
/* fade */
|
|
1108
|
+
"a"])(offBlack, 0.32),
|
|
1109
|
+
offBlack16: Object(_util_utils_js__WEBPACK_IMPORTED_MODULE_0__[
|
|
1110
|
+
/* fade */
|
|
1111
|
+
"a"])(offBlack, 0.16),
|
|
1112
|
+
offBlack8: Object(_util_utils_js__WEBPACK_IMPORTED_MODULE_0__[
|
|
1113
|
+
/* fade */
|
|
1114
|
+
"a"])(offBlack, 0.08),
|
|
1115
|
+
offWhite: "#f7f8fa",
|
|
1116
|
+
white,
|
|
1117
|
+
white64: Object(_util_utils_js__WEBPACK_IMPORTED_MODULE_0__[
|
|
1118
|
+
/* fade */
|
|
1119
|
+
"a"])(white, 0.64),
|
|
1120
|
+
white50: Object(_util_utils_js__WEBPACK_IMPORTED_MODULE_0__[
|
|
1121
|
+
/* fade */
|
|
1122
|
+
"a"])(white, 0.5),
|
|
1123
|
+
// Brand
|
|
1124
|
+
darkBlue: "#0a2a66",
|
|
1125
|
+
teal: "#14bf96",
|
|
1126
|
+
lightBlue: "#37c5fd",
|
|
1127
|
+
pink: "#fa50ae"
|
|
1128
|
+
};
|
|
1129
|
+
const SemanticColor = {
|
|
1130
|
+
controlDefault: Color.blue,
|
|
1131
|
+
controlDestructive: Color.red
|
|
1132
|
+
};
|
|
1133
|
+
/***/
|
|
1134
|
+
}
|
|
1135
|
+
/******/
|
|
1136
|
+
]);
|
|
1137
|
+
|
|
814
1138
|
/***/ }),
|
|
815
1139
|
/* 7 */
|
|
816
1140
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
@@ -828,6 +1152,7 @@ const SemanticColor = {
|
|
|
828
1152
|
/* harmony import */ var _khanacademy_wonder_blocks_core__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(5);
|
|
829
1153
|
/* harmony import */ var _khanacademy_wonder_blocks_core__WEBPACK_IMPORTED_MODULE_4___default = /*#__PURE__*/__webpack_require__.n(_khanacademy_wonder_blocks_core__WEBPACK_IMPORTED_MODULE_4__);
|
|
830
1154
|
/* harmony import */ var _khanacademy_wonder_blocks_color__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(6);
|
|
1155
|
+
/* harmony import */ var _khanacademy_wonder_blocks_color__WEBPACK_IMPORTED_MODULE_5___default = /*#__PURE__*/__webpack_require__.n(_khanacademy_wonder_blocks_color__WEBPACK_IMPORTED_MODULE_5__);
|
|
831
1156
|
/* harmony import */ var _util_get_clickable_behavior_js__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(3);
|
|
832
1157
|
/* harmony import */ var _util_is_client_side_url_js__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(1);
|
|
833
1158
|
function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
|
|
@@ -894,7 +1219,7 @@ class Clickable extends react__WEBPACK_IMPORTED_MODULE_0__["Component"] {
|
|
|
894
1219
|
} else {
|
|
895
1220
|
return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0__["createElement"](StyledButton, _extends({}, commonProps, {
|
|
896
1221
|
type: "button",
|
|
897
|
-
disabled: this.props.disabled
|
|
1222
|
+
"aria-disabled": this.props.disabled
|
|
898
1223
|
}), this.props.children(clickableState));
|
|
899
1224
|
}
|
|
900
1225
|
};
|
|
@@ -996,10 +1321,10 @@ const styles = aphrodite__WEBPACK_IMPORTED_MODULE_1__["StyleSheet"].create({
|
|
|
996
1321
|
cursor: "pointer"
|
|
997
1322
|
},
|
|
998
1323
|
focused: {
|
|
999
|
-
outline: `solid 2px ${
|
|
1324
|
+
outline: `solid 2px ${_khanacademy_wonder_blocks_color__WEBPACK_IMPORTED_MODULE_5___default.a.blue}`
|
|
1000
1325
|
},
|
|
1001
1326
|
focusedLight: {
|
|
1002
|
-
outline: `solid 2px ${
|
|
1327
|
+
outline: `solid 2px ${_khanacademy_wonder_blocks_color__WEBPACK_IMPORTED_MODULE_5___default.a.white}`
|
|
1003
1328
|
}
|
|
1004
1329
|
});
|
|
1005
1330
|
|
|
@@ -1017,32 +1342,6 @@ module.exports = require("react-router");
|
|
|
1017
1342
|
|
|
1018
1343
|
/***/ }),
|
|
1019
1344
|
/* 10 */
|
|
1020
|
-
/***/ (function(module, exports) {
|
|
1021
|
-
|
|
1022
|
-
function _extends() {
|
|
1023
|
-
module.exports = _extends = Object.assign || function (target) {
|
|
1024
|
-
for (var i = 1; i < arguments.length; i++) {
|
|
1025
|
-
var source = arguments[i];
|
|
1026
|
-
|
|
1027
|
-
for (var key in source) {
|
|
1028
|
-
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
|
1029
|
-
target[key] = source[key];
|
|
1030
|
-
}
|
|
1031
|
-
}
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
|
-
return target;
|
|
1035
|
-
};
|
|
1036
|
-
|
|
1037
|
-
module.exports["default"] = module.exports, module.exports.__esModule = true;
|
|
1038
|
-
return _extends.apply(this, arguments);
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
module.exports = _extends;
|
|
1042
|
-
module.exports["default"] = module.exports, module.exports.__esModule = true;
|
|
1043
|
-
|
|
1044
|
-
/***/ }),
|
|
1045
|
-
/* 11 */
|
|
1046
1345
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
1047
1346
|
|
|
1048
1347
|
"use strict";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@khanacademy/wonder-blocks-clickable",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.4",
|
|
4
4
|
"design": "v1",
|
|
5
5
|
"description": "Clickable component for Wonder-Blocks.",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -15,8 +15,8 @@
|
|
|
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.2.1"
|
|
20
20
|
},
|
|
21
21
|
"peerDependencies": {
|
|
22
22
|
"aphrodite": "^1.2.5",
|
|
@@ -26,7 +26,6 @@
|
|
|
26
26
|
"react-router-dom": "5.3.0"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
|
-
"wb-dev-build-settings": "^0.
|
|
30
|
-
}
|
|
31
|
-
"gitHead": "b6193f70c73e70fbaf76bc688dc69a47fb1d0ef3"
|
|
29
|
+
"wb-dev-build-settings": "^0.3.0"
|
|
30
|
+
}
|
|
32
31
|
}
|
|
@@ -21,9 +21,9 @@ exports[`wonder-blocks-clickable example 1 1`] = `
|
|
|
21
21
|
}
|
|
22
22
|
>
|
|
23
23
|
<button
|
|
24
|
+
aria-disabled={false}
|
|
24
25
|
aria-label=""
|
|
25
26
|
className=""
|
|
26
|
-
disabled={false}
|
|
27
27
|
onBlur={[Function]}
|
|
28
28
|
onClick={[Function]}
|
|
29
29
|
onDragStart={[Function]}
|
|
@@ -128,9 +128,9 @@ exports[`wonder-blocks-clickable example 2 1`] = `
|
|
|
128
128
|
}
|
|
129
129
|
>
|
|
130
130
|
<button
|
|
131
|
+
aria-disabled={false}
|
|
131
132
|
aria-label=""
|
|
132
133
|
className=""
|
|
133
|
-
disabled={false}
|
|
134
134
|
onBlur={[Function]}
|
|
135
135
|
onClick={[Function]}
|
|
136
136
|
onDragStart={[Function]}
|
|
@@ -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(
|
|
@@ -335,7 +378,7 @@ describe("ClickableBehavior", () => {
|
|
|
335
378
|
expect(button.state("pressed")).toEqual(false);
|
|
336
379
|
|
|
337
380
|
button.simulate("focus");
|
|
338
|
-
expect(button.state("focused")).toEqual(
|
|
381
|
+
expect(button.state("focused")).toEqual(true);
|
|
339
382
|
|
|
340
383
|
const anchor = shallow(
|
|
341
384
|
<ClickableBehavior
|
|
@@ -421,7 +464,7 @@ describe("ClickableBehavior", () => {
|
|
|
421
464
|
|
|
422
465
|
expect(button.state("hovered")).toEqual(false);
|
|
423
466
|
expect(button.state("pressed")).toEqual(false);
|
|
424
|
-
expect(button.state("focused")).toEqual(
|
|
467
|
+
expect(button.state("focused")).toEqual(true);
|
|
425
468
|
});
|
|
426
469
|
|
|
427
470
|
describe("full page load navigation", () => {
|
|
@@ -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,9 @@
|
|
|
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";
|
|
6
|
+
import {render, screen} from "@testing-library/react";
|
|
7
|
+
import userEvent from "@testing-library/user-event";
|
|
5
8
|
|
|
6
9
|
import {View} from "@khanacademy/wonder-blocks-core";
|
|
7
10
|
import Clickable from "../clickable.js";
|
|
@@ -13,6 +16,11 @@ const wait = (delay: number = 0) =>
|
|
|
13
16
|
});
|
|
14
17
|
|
|
15
18
|
describe("Clickable", () => {
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
delete window.location;
|
|
21
|
+
window.location = {assign: jest.fn()};
|
|
22
|
+
});
|
|
23
|
+
|
|
16
24
|
test("client-side navigation", () => {
|
|
17
25
|
// Arrange
|
|
18
26
|
const wrapper = mount(
|
|
@@ -129,8 +137,6 @@ describe("Clickable", () => {
|
|
|
129
137
|
|
|
130
138
|
test("should navigate to a specific link using the keyboard", () => {
|
|
131
139
|
// Arrange
|
|
132
|
-
window.location.assign = jest.fn();
|
|
133
|
-
|
|
134
140
|
const wrapper = mount(
|
|
135
141
|
<Clickable testId="button" href="/foo" skipClientNav={true}>
|
|
136
142
|
{(eventState) => <h1>Click Me!</h1>}
|
|
@@ -276,7 +282,6 @@ describe("Clickable", () => {
|
|
|
276
282
|
|
|
277
283
|
test("safeWithNav with skipClientNav=true waits for promise resolution", async () => {
|
|
278
284
|
// Arrange
|
|
279
|
-
jest.spyOn(window.location, "assign");
|
|
280
285
|
const wrapper = mount(
|
|
281
286
|
<MemoryRouter>
|
|
282
287
|
<div>
|
|
@@ -309,7 +314,6 @@ describe("Clickable", () => {
|
|
|
309
314
|
|
|
310
315
|
test("beforeNav resolution and safeWithNav with skipClientNav=true waits for promise resolution", async () => {
|
|
311
316
|
// Arrange
|
|
312
|
-
jest.spyOn(window.location, "assign");
|
|
313
317
|
const wrapper = mount(
|
|
314
318
|
<MemoryRouter>
|
|
315
319
|
<div>
|
|
@@ -336,8 +340,6 @@ describe("Clickable", () => {
|
|
|
336
340
|
buttonWrapper.simulate("click", {button: 0});
|
|
337
341
|
await wait(0);
|
|
338
342
|
buttonWrapper.update();
|
|
339
|
-
await wait(0);
|
|
340
|
-
buttonWrapper.update();
|
|
341
343
|
|
|
342
344
|
// Assert
|
|
343
345
|
expect(window.location.assign).toHaveBeenCalledWith("/foo");
|
|
@@ -345,7 +347,6 @@ describe("Clickable", () => {
|
|
|
345
347
|
|
|
346
348
|
test("safeWithNav with skipClientNav=true waits for promise rejection", async () => {
|
|
347
349
|
// Arrange
|
|
348
|
-
jest.spyOn(window.location, "assign");
|
|
349
350
|
const wrapper = mount(
|
|
350
351
|
<MemoryRouter>
|
|
351
352
|
<div>
|
|
@@ -376,9 +377,8 @@ describe("Clickable", () => {
|
|
|
376
377
|
expect(window.location.assign).toHaveBeenCalledWith("/foo");
|
|
377
378
|
});
|
|
378
379
|
|
|
379
|
-
test("safeWithNav with skipClientNav=false calls safeWithNav but doesn't wait to navigate",
|
|
380
|
+
test("safeWithNav with skipClientNav=false calls safeWithNav but doesn't wait to navigate", () => {
|
|
380
381
|
// Arrange
|
|
381
|
-
jest.spyOn(window.location, "assign");
|
|
382
382
|
const safeWithNavMock = jest.fn();
|
|
383
383
|
const wrapper = mount(
|
|
384
384
|
<MemoryRouter>
|
|
@@ -406,12 +406,16 @@ describe("Clickable", () => {
|
|
|
406
406
|
|
|
407
407
|
// Assert
|
|
408
408
|
expect(safeWithNavMock).toHaveBeenCalled();
|
|
409
|
-
expect(
|
|
409
|
+
expect(wrapper).toIncludeText(
|
|
410
|
+
"Hello, world!" /*client side nav to /foo*/,
|
|
411
|
+
);
|
|
412
|
+
expect(window.location.assign).not.toHaveBeenCalledWith(
|
|
413
|
+
"/foo" /*not a full page nav*/,
|
|
414
|
+
);
|
|
410
415
|
});
|
|
411
416
|
|
|
412
417
|
test("safeWithNav with beforeNav resolution and skipClientNav=false calls safeWithNav but doesn't wait to navigate", async () => {
|
|
413
418
|
// Arrange
|
|
414
|
-
jest.spyOn(window.location, "assign");
|
|
415
419
|
const safeWithNavMock = jest.fn();
|
|
416
420
|
const wrapper = mount(
|
|
417
421
|
<MemoryRouter>
|
|
@@ -442,7 +446,53 @@ describe("Clickable", () => {
|
|
|
442
446
|
|
|
443
447
|
// Assert
|
|
444
448
|
expect(safeWithNavMock).toHaveBeenCalled();
|
|
445
|
-
expect(
|
|
449
|
+
expect(wrapper).toIncludeText(
|
|
450
|
+
"Hello, world!" /*client side nav to /foo*/,
|
|
451
|
+
);
|
|
452
|
+
expect(window.location.assign).not.toHaveBeenCalledWith(
|
|
453
|
+
"/foo" /*not a full page nav*/,
|
|
454
|
+
);
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
test("should add aria-disabled if disabled is set", () => {
|
|
458
|
+
// Arrange
|
|
459
|
+
|
|
460
|
+
// Act
|
|
461
|
+
render(
|
|
462
|
+
<Clickable testId="clickable-button" disabled={true}>
|
|
463
|
+
{(eventState) => <h1>Click Me!</h1>}
|
|
464
|
+
</Clickable>,
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
const button = screen.getByTestId("clickable-button");
|
|
468
|
+
|
|
469
|
+
// Assert
|
|
470
|
+
expect(button).toHaveAttribute("aria-disabled", "true");
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
test("allow keyboard navigation when disabled is set", () => {
|
|
474
|
+
// Arrange
|
|
475
|
+
render(
|
|
476
|
+
<div>
|
|
477
|
+
<button>First focusable button</button>
|
|
478
|
+
<Clickable testId="clickable-button" disabled={true}>
|
|
479
|
+
{(eventState) => <h1>Click Me!</h1>}
|
|
480
|
+
</Clickable>
|
|
481
|
+
</div>,
|
|
482
|
+
);
|
|
483
|
+
|
|
484
|
+
// Act
|
|
485
|
+
// RTL's focuses on `document.body` by default, so we need to focus on
|
|
486
|
+
// the first button
|
|
487
|
+
userEvent.tab();
|
|
488
|
+
|
|
489
|
+
// Then we focus on our Clickable button.
|
|
490
|
+
userEvent.tab();
|
|
491
|
+
|
|
492
|
+
const button = screen.getByTestId("clickable-button");
|
|
493
|
+
|
|
494
|
+
// Assert
|
|
495
|
+
expect(button).toHaveFocus();
|
|
446
496
|
});
|
|
447
497
|
|
|
448
498
|
describe("raw events", () => {
|
|
@@ -221,9 +221,9 @@ const disabledHandlers = {
|
|
|
221
221
|
onTouchCancel: () => void 0,
|
|
222
222
|
onKeyDown: () => void 0,
|
|
223
223
|
onKeyUp: () => void 0,
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
tabIndex:
|
|
224
|
+
// Clickable components should still be tabbable so they can
|
|
225
|
+
// be used as anchors.
|
|
226
|
+
tabIndex: 0,
|
|
227
227
|
};
|
|
228
228
|
|
|
229
229
|
const keyCodes = {
|
|
@@ -332,9 +332,10 @@ export default class ClickableBehavior extends React.Component<
|
|
|
332
332
|
props: Props,
|
|
333
333
|
state: ClickableState,
|
|
334
334
|
): ?Partial<ClickableState> {
|
|
335
|
-
// If new props are disabled, reset the hovered/
|
|
335
|
+
// If new props are disabled, reset the hovered/pressed states
|
|
336
336
|
if (props.disabled) {
|
|
337
|
-
|
|
337
|
+
// Keep the focused state for enabling keyboard navigation.
|
|
338
|
+
return {...startState, focused: state.focused};
|
|
338
339
|
} else {
|
|
339
340
|
// Cannot return undefined
|
|
340
341
|
return null;
|
|
@@ -558,9 +559,8 @@ export default class ClickableBehavior extends React.Component<
|
|
|
558
559
|
}
|
|
559
560
|
|
|
560
561
|
const keyCode = e.which || e.keyCode;
|
|
561
|
-
const {triggerOnEnter, triggerOnSpace} =
|
|
562
|
-
role
|
|
563
|
-
);
|
|
562
|
+
const {triggerOnEnter, triggerOnSpace} =
|
|
563
|
+
getAppropriateTriggersForRole(role);
|
|
564
564
|
if (
|
|
565
565
|
(triggerOnEnter && keyCode === keyCodes.enter) ||
|
|
566
566
|
(triggerOnSpace && keyCode === keyCodes.space)
|
|
@@ -585,9 +585,8 @@ export default class ClickableBehavior extends React.Component<
|
|
|
585
585
|
}
|
|
586
586
|
|
|
587
587
|
const keyCode = e.which || e.keyCode;
|
|
588
|
-
const {triggerOnEnter, triggerOnSpace} =
|
|
589
|
-
role
|
|
590
|
-
);
|
|
588
|
+
const {triggerOnEnter, triggerOnSpace} =
|
|
589
|
+
getAppropriateTriggersForRole(role);
|
|
591
590
|
if (
|
|
592
591
|
(triggerOnEnter && keyCode === keyCodes.enter) ||
|
|
593
592
|
(triggerOnSpace && keyCode === keyCodes.space)
|
|
@@ -610,7 +609,12 @@ export default class ClickableBehavior extends React.Component<
|
|
|
610
609
|
|
|
611
610
|
render(): React.Node {
|
|
612
611
|
const childrenProps: ChildrenProps = this.props.disabled
|
|
613
|
-
?
|
|
612
|
+
? {
|
|
613
|
+
...disabledHandlers,
|
|
614
|
+
// Keep these handlers for keyboard accessibility.
|
|
615
|
+
onFocus: this.handleFocus,
|
|
616
|
+
onBlur: this.handleBlur,
|
|
617
|
+
}
|
|
614
618
|
: {
|
|
615
619
|
onClick: this.handleClick,
|
|
616
620
|
onMouseEnter: this.handleMouseEnter,
|
|
@@ -252,7 +252,7 @@ export default class Clickable extends React.Component<Props> {
|
|
|
252
252
|
<StyledButton
|
|
253
253
|
{...commonProps}
|
|
254
254
|
type="button"
|
|
255
|
-
disabled={this.props.disabled}
|
|
255
|
+
aria-disabled={this.props.disabled}
|
|
256
256
|
>
|
|
257
257
|
{this.props.children(clickableState)}
|
|
258
258
|
</StyledButton>
|
|
@@ -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
|
|
@@ -15,7 +15,7 @@ export default {
|
|
|
15
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,14 +77,14 @@ export const keyboardNavigation: StoryComponentType = () => (
|
|
|
77
77
|
</View>
|
|
78
78
|
);
|
|
79
79
|
|
|
80
|
-
|
|
80
|
+
KeyboardNavigation.parameters = {
|
|
81
81
|
chromatic: {
|
|
82
82
|
// we don't need screenshots because this story only tests behavior.
|
|
83
83
|
disableSnapshot: true,
|
|
84
84
|
},
|
|
85
85
|
};
|
|
86
86
|
|
|
87
|
-
export const
|
|
87
|
+
export const KeyboardNavigationTab: StoryComponentType = () => (
|
|
88
88
|
<View>
|
|
89
89
|
<Clickable role="tab" aria-controls="panel-1" id="tab-1">
|
|
90
90
|
{({hovered, focused, pressed}) => (
|
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.
|