diff --git a/ActionButton.js b/ActionButton.js
index b8306c2..d384ac0 100644
--- a/ActionButton.js
+++ b/ActionButton.js
@@ -6,6 +6,10 @@ import {
View,
Animated,
TouchableOpacity,
+ findNodeHandle,
+ UIManager,
+ Platform,
+ AccessibilityInfo,
} from "react-native";
import ActionButtonItem from "./ActionButtonItem";
import {
@@ -14,18 +18,52 @@ import {
getTouchableComponent,
isAndroid,
touchableBackground,
- DEFAULT_ACTIVE_OPACITY
+ DEFAULT_ACTIVE_OPACITY,
} from "./shared";
+//////////////////////
+// HELPER FUNCTIONS
+//////////////////////
+
+const focusOnView = (ref) => {
+ if (!ref) {
+ console.warn('ref is null');
+ return;
+ }
+ const reactTag = findNodeHandle(ref);
+
+ Platform.OS === 'android' ? UIManager.sendAccessibilityEvent(
+ reactTag,
+ 8
+ ) : AccessibilityInfo.setAccessibilityFocus(reactTag)
+};
+
+const filterActionButtons = (children) => {
+ const actionButtons = !Array.isArray(children) ? [children] : children;
+ return actionButtons.filter(actionButton => (typeof actionButton === 'object'));
+};
+
export default class ActionButton extends Component {
constructor(props) {
super(props);
+ const {
+ children,
+ } = props;
+
this.state = {
resetToken: props.resetToken,
- active: props.active
+ active: props.active,
};
+ const actionButtons = filterActionButtons(children);
+
+ this.refIndexes = [];
+ actionButtons.forEach((button, idx) => {
+ this[`actionButton${idx}Ref`] = React.createRef();
+ this.refIndexes.push(idx);
+ });
+
this.anim = new Animated.Value(props.active ? 1 : 0);
this.timeout = null;
}
@@ -34,25 +72,32 @@ export default class ActionButton extends Component {
this.mounted = true;
}
- componentWillUnmount() {
- this.mounted = false;
- clearTimeout(this.timeout);
- }
-
componentWillReceiveProps(nextProps) {
- if (nextProps.resetToken !== this.state.resetToken) {
- if (nextProps.active === false && this.state.active === true) {
- if (this.props.onReset) this.props.onReset();
+ const {
+ resetToken,
+ active,
+ } = this.state;
+
+ const {
+ onReset,
+ } = this.props;
+
+ if (nextProps.resetToken !== resetToken) {
+ if (nextProps.active === false && active === true) {
+ if (onReset) {
+ onReset();
+ }
Animated.spring(this.anim, { toValue: 0 }).start();
- setTimeout(
- () =>
- this.setState({ active: false, resetToken: nextProps.resetToken }),
- 250
- );
+ setTimeout(() => {
+ this.setState({
+ active: false,
+ resetToken: nextProps.resetToken,
+ });
+ }, 250);
return;
}
- if (nextProps.active === true && this.state.active === false) {
+ if (nextProps.active === true && active === false) {
Animated.spring(this.anim, { toValue: 1 }).start();
this.setState({ active: true, resetToken: nextProps.resetToken });
return;
@@ -60,11 +105,32 @@ export default class ActionButton extends Component {
this.setState({
resetToken: nextProps.resetToken,
- active: nextProps.active
+ active: nextProps.active,
});
}
}
+ componentDidUpdate(prevProps, prevState) {
+ const {
+ active,
+ } = this.state;
+
+ if (prevState.active !== active && active) {
+ setTimeout(() => {
+ this.focusOnTopActionButton();
+ }, 500);
+
+ setTimeout(() => {
+ this.announceActionButtons();
+ }, 3000);
+ }
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ clearTimeout(this.timeout);
+ }
+
//////////////////////
// STYLESHEET GETTERS
//////////////////////
@@ -93,6 +159,45 @@ export default class ActionButton extends Component {
];
}
+ //////////////////////
+ // ACCESSIBILITY METHODS
+ //////////////////////
+
+ announceActionButtons() {
+ const {
+ children,
+ announceActionsLabel,
+ } = this.props;
+
+ const actionButtons = filterActionButtons(children);
+
+ const actionButtonsAccessibilityAnnouncement = actionButtons.reduce((acc, actionButton) => {
+ return `${acc} ${actionButton.props.accessibilityLabel}, `;
+ }, announceActionsLabel);
+
+ if (actionButtons.length && Platform.OS === 'ios') {
+ AccessibilityInfo.announceForAccessibility(actionButtonsAccessibilityAnnouncement);
+ }
+ }
+
+ focusOnTopActionButton() {
+ const {
+ active,
+ } = this.state;
+
+ const actionButtonRefs = this.refIndexes.reduce((acc, refIdx) => {
+ const buttonRef = this[`actionButton${refIdx}Ref`].current;
+ if (!!buttonRef) {
+ acc = [...acc, buttonRef];
+ }
+ return acc;
+ }, []);
+
+ if (active && actionButtonRefs.length) {
+ focusOnView(actionButtonRefs[0]);
+ }
+ }
+
//////////////////////
// RENDER METHODS
//////////////////////
@@ -142,6 +247,7 @@ export default class ActionButton extends Component {
);
}
+
_renderMainButton() {
const animatedViewStyle = {
transform: [
@@ -261,9 +367,7 @@ export default class ActionButton extends Component {
if (!this.state.active) return null;
- let actionButtons = !Array.isArray(children) ? [children] : children;
-
- actionButtons = actionButtons.filter( actionButton => (typeof actionButton == 'object') )
+ const actionButtons = filterActionButtons(children);
const actionStyle = {
flex: 1,
@@ -284,6 +388,7 @@ export default class ActionButton extends Component {
anim={this.anim}
{...this.props}
{...ActionButton.props}
+ buttonRef={this[`actionButton${idx}Ref`]}
parentSize={this.props.size}
btnColor={this.props.btnOutRange}
onPress={() => {
@@ -386,6 +491,7 @@ ActionButton.propTypes = {
testID: PropTypes.string,
accessibilityLabel: PropTypes.string,
+ announceActionsLabel: PropTypes.string,
accessible: PropTypes.bool
};
@@ -417,6 +523,7 @@ ActionButton.defaultProps = {
nativeFeedbackRippleColor: "rgba(255,255,255,0.75)",
testID: undefined,
accessibilityLabel: undefined,
+ announceActionsLabel: 'Available actions from top to bottom: ',
accessible: undefined
};
@@ -434,4 +541,4 @@ const styles = StyleSheet.create({
fontSize: 24,
backgroundColor: "transparent"
}
-});
+});
\ No newline at end of file
diff --git a/ActionButtonItem.js b/ActionButtonItem.js
index 7475bff..e75e9c4 100644
--- a/ActionButtonItem.js
+++ b/ActionButtonItem.js
@@ -29,6 +29,7 @@ const TextTouchable = isAndroid
export default class ActionButtonItem extends Component {
static get defaultProps() {
return {
+ buttonRef: null,
active: true,
spaceBetween: 15,
useNativeFeedback: true,
@@ -40,6 +41,7 @@ export default class ActionButtonItem extends Component {
static get propTypes() {
return {
+ buttonRef: PropTypes.any,
active: PropTypes.bool,
useNativeFeedback: PropTypes.bool,
fixNativeFeedbackRadius: PropTypes.bool,
@@ -48,8 +50,70 @@ export default class ActionButtonItem extends Component {
};
}
+ _renderTitle() {
+ if (!this.props.title) return null;
+
+ const {
+ textContainerStyle,
+ hideLabelShadow,
+ offsetX,
+ parentSize,
+ size,
+ position,
+ spaceBetween
+ } = this.props;
+ const offsetTop = Math.max(size / 2 - TEXT_HEIGHT / 2, 0);
+ const positionStyles = { top: offsetTop };
+ const hideShadow = hideLabelShadow === undefined
+ ? this.props.hideShadow
+ : hideLabelShadow;
+
+ if (position !== "center") {
+ positionStyles[position] =
+ offsetX + (parentSize - size) / 2 + size + spaceBetween;
+ } else {
+ positionStyles.right = WIDTH / 2 + size / 2 + spaceBetween;
+ }
+
+ const textStyles = [
+ styles.textContainer,
+ positionStyles,
+ !hideShadow && shadowStyle,
+ textContainerStyle
+ ];
+
+ const title = (
+ React.isValidElement(this.props.title) ?
+ this.props.title
+ : (
+
+ {this.props.title}
+
+ )
+ )
+
+ return (
+
+
+ {title}
+
+
+ );
+ }
+
render() {
const {
+ buttonRef,
size,
position,
verticalOrientation,
@@ -101,6 +165,7 @@ export default class ActionButtonItem extends Component {
paddingHorizontal: this.props.offsetX,
height: size + SHADOW_SPACE + spacing
};
+
return (
);
}
-
- _renderTitle() {
- if (!this.props.title) return null;
-
- const {
- textContainerStyle,
- hideLabelShadow,
- offsetX,
- parentSize,
- size,
- position,
- spaceBetween
- } = this.props;
- const offsetTop = Math.max(size / 2 - TEXT_HEIGHT / 2, 0);
- const positionStyles = { top: offsetTop };
- const hideShadow = hideLabelShadow === undefined
- ? this.props.hideShadow
- : hideLabelShadow;
-
- if (position !== "center") {
- positionStyles[position] =
- offsetX + (parentSize - size) / 2 + size + spaceBetween;
- } else {
- positionStyles.right = WIDTH / 2 + size / 2 + spaceBetween;
- }
-
- const textStyles = [
- styles.textContainer,
- positionStyles,
- !hideShadow && shadowStyle,
- textContainerStyle
- ];
-
- const title = (
- React.isValidElement(this.props.title) ?
- this.props.title
- : (
-
- {this.props.title}
-
- )
- )
-
- return (
-
-
- {title}
-
-
- );
- }
}
const styles = StyleSheet.create({
@@ -208,4 +213,4 @@ const styles = StyleSheet.create({
fontSize: 12,
color: "#444"
}
-});
+});
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index cc96fdf..79b4d96 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -19,7 +19,7 @@
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz",
"integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=",
"requires": {
- "iconv-lite": "0.4.23"
+ "iconv-lite": "~0.4.13"
}
},
"fbjs": {
@@ -27,13 +27,13 @@
"resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.16.tgz",
"integrity": "sha1-XmdDL1UNxBtXK/VYR7ispk5TN9s=",
"requires": {
- "core-js": "1.2.7",
- "isomorphic-fetch": "2.2.1",
- "loose-envify": "1.3.1",
- "object-assign": "4.1.1",
- "promise": "7.3.1",
- "setimmediate": "1.0.5",
- "ua-parser-js": "0.7.18"
+ "core-js": "^1.0.0",
+ "isomorphic-fetch": "^2.1.1",
+ "loose-envify": "^1.0.0",
+ "object-assign": "^4.1.0",
+ "promise": "^7.1.1",
+ "setimmediate": "^1.0.5",
+ "ua-parser-js": "^0.7.9"
}
},
"iconv-lite": {
@@ -41,7 +41,7 @@
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz",
"integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==",
"requires": {
- "safer-buffer": "2.1.2"
+ "safer-buffer": ">= 2.1.2 < 3"
}
},
"is-stream": {
@@ -54,8 +54,8 @@
"resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz",
"integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=",
"requires": {
- "node-fetch": "1.7.3",
- "whatwg-fetch": "2.0.4"
+ "node-fetch": "^1.0.1",
+ "whatwg-fetch": ">=0.10.0"
}
},
"js-tokens": {
@@ -68,7 +68,7 @@
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz",
"integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=",
"requires": {
- "js-tokens": "3.0.2"
+ "js-tokens": "^3.0.0"
}
},
"node-fetch": {
@@ -76,8 +76,8 @@
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz",
"integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==",
"requires": {
- "encoding": "0.1.12",
- "is-stream": "1.1.0"
+ "encoding": "^0.1.11",
+ "is-stream": "^1.0.1"
}
},
"object-assign": {
@@ -90,7 +90,7 @@
"resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
"integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
"requires": {
- "asap": "2.0.6"
+ "asap": "~2.0.3"
}
},
"prop-types": {
@@ -98,9 +98,9 @@
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.1.tgz",
"integrity": "sha512-4ec7bY1Y66LymSUOH/zARVYObB23AT2h8cf6e/O6ZALB/N0sqZFEx7rq6EYPX2MkOdKORuooI/H5k9TlR4q7kQ==",
"requires": {
- "fbjs": "0.8.16",
- "loose-envify": "1.3.1",
- "object-assign": "4.1.1"
+ "fbjs": "^0.8.16",
+ "loose-envify": "^1.3.1",
+ "object-assign": "^4.1.1"
}
},
"safer-buffer": {