|
1 |
| -import React from 'react'; |
| 1 | +import React, { useEffect, useState, useRef, useContext } from 'react'; |
2 | 2 | import qsa from 'dom-helpers/query/querySelectorAll';
|
3 | 3 | import PropTypes from 'prop-types';
|
| 4 | +import useMergedRefs from '@restart/hooks/useMergedRefs'; |
4 | 5 |
|
5 |
| -import mapContextToProps from '@restart/context/mapContextToProps'; |
6 | 6 | import SelectableContext, { makeEventKey } from './SelectableContext';
|
7 | 7 | import NavContext from './NavContext';
|
8 | 8 | import TabContext from './TabContext';
|
9 | 9 |
|
10 | 10 | const noop = () => {};
|
11 | 11 |
|
12 |
| -class AbstractNav extends React.Component { |
13 |
| - static propTypes = { |
14 |
| - onSelect: PropTypes.func.isRequired, |
15 |
| - |
16 |
| - as: PropTypes.elementType, |
17 |
| - |
18 |
| - /** @private */ |
19 |
| - onKeyDown: PropTypes.func, |
20 |
| - /** @private */ |
21 |
| - parentOnSelect: PropTypes.func, |
22 |
| - /** @private */ |
23 |
| - getControlledId: PropTypes.func, |
24 |
| - /** @private */ |
25 |
| - getControllerId: PropTypes.func, |
26 |
| - /** @private */ |
27 |
| - activeKey: PropTypes.any, |
28 |
| - }; |
29 |
| - |
30 |
| - state = { |
31 |
| - navContext: null, |
32 |
| - }; |
33 |
| - |
34 |
| - static getDerivedStateFromProps({ |
35 |
| - activeKey, |
36 |
| - getControlledId, |
37 |
| - getControllerId, |
38 |
| - role, |
39 |
| - }) { |
40 |
| - return { |
41 |
| - navContext: { |
42 |
| - role, // used by NavLink to determine it's role |
43 |
| - activeKey: makeEventKey(activeKey), |
44 |
| - getControlledId: getControlledId || noop, |
45 |
| - getControllerId: getControllerId || noop, |
46 |
| - }, |
47 |
| - }; |
48 |
| - } |
49 |
| - |
50 |
| - componentDidUpdate() { |
51 |
| - if (!this._needsRefocus || !this.listNode) return; |
52 |
| - |
53 |
| - let activeChild = this.listNode.querySelector('[data-rb-event-key].active'); |
54 |
| - if (activeChild) activeChild.focus(); |
55 |
| - } |
56 |
| - |
57 |
| - getNextActiveChild(offset) { |
58 |
| - if (!this.listNode) return null; |
59 |
| - |
60 |
| - let items = qsa(this.listNode, '[data-rb-event-key]:not(.disabled)'); |
61 |
| - let activeChild = this.listNode.querySelector('.active'); |
62 |
| - |
63 |
| - let index = items.indexOf(activeChild); |
64 |
| - if (index === -1) return null; |
65 |
| - |
66 |
| - let nextIndex = index + offset; |
67 |
| - if (nextIndex >= items.length) nextIndex = 0; |
68 |
| - if (nextIndex < 0) nextIndex = items.length - 1; |
69 |
| - return items[nextIndex]; |
70 |
| - } |
71 |
| - |
72 |
| - handleSelect = (key, event) => { |
73 |
| - const { onSelect, parentOnSelect } = this.props; |
74 |
| - if (key == null) return; |
75 |
| - if (onSelect) onSelect(key, event); |
76 |
| - if (parentOnSelect) parentOnSelect(key, event); |
77 |
| - }; |
78 |
| - |
79 |
| - handleKeyDown = event => { |
80 |
| - const { onKeyDown } = this.props; |
81 |
| - if (onKeyDown) onKeyDown(event); |
82 |
| - |
83 |
| - let nextActiveChild; |
84 |
| - switch (event.key) { |
85 |
| - case 'ArrowLeft': |
86 |
| - case 'ArrowUp': |
87 |
| - nextActiveChild = this.getNextActiveChild(-1); |
88 |
| - break; |
89 |
| - case 'ArrowRight': |
90 |
| - case 'ArrowDown': |
91 |
| - nextActiveChild = this.getNextActiveChild(1); |
92 |
| - break; |
93 |
| - default: |
94 |
| - return; |
95 |
| - } |
96 |
| - if (!nextActiveChild) return; |
| 12 | +const propTypes = { |
| 13 | + onSelect: PropTypes.func.isRequired, |
| 14 | + |
| 15 | + as: PropTypes.elementType, |
| 16 | + |
| 17 | + role: PropTypes.string, |
97 | 18 |
|
98 |
| - event.preventDefault(); |
99 |
| - this.handleSelect(nextActiveChild.dataset.rbEventKey, event); |
100 |
| - this._needsRefocus = true; |
101 |
| - }; |
| 19 | + /** @private */ |
| 20 | + onKeyDown: PropTypes.func, |
| 21 | + /** @private */ |
| 22 | + parentOnSelect: PropTypes.func, |
| 23 | + /** @private */ |
| 24 | + getControlledId: PropTypes.func, |
| 25 | + /** @private */ |
| 26 | + getControllerId: PropTypes.func, |
| 27 | + /** @private */ |
| 28 | + activeKey: PropTypes.any, |
| 29 | +}; |
102 | 30 |
|
103 |
| - attachRef = ref => { |
104 |
| - this.listNode = ref; |
105 |
| - }; |
| 31 | +const defaultProps = { |
| 32 | + role: 'tablist', |
| 33 | +}; |
106 | 34 |
|
107 |
| - render() { |
108 |
| - const { |
| 35 | +const AbstractNav = React.forwardRef( |
| 36 | + ( |
| 37 | + { |
109 | 38 | // Need to define the default "as" during prop destructuring to be compatible with styled-components github.com/react-bootstrap/react-bootstrap/issues/3595
|
110 | 39 | as: Component = 'ul',
|
111 |
| - onSelect: _, |
112 |
| - parentOnSelect: _0, |
113 |
| - getControlledId: _1, |
114 |
| - getControllerId: _2, |
115 |
| - activeKey: _3, |
| 40 | + onSelect, |
| 41 | + activeKey, |
| 42 | + role, |
| 43 | + onKeyDown, |
116 | 44 | ...props
|
117 |
| - } = this.props; |
118 |
| - |
119 |
| - if (props.role === 'tablist') { |
120 |
| - props.onKeyDown = this.handleKeyDown; |
| 45 | + }, |
| 46 | + ref, |
| 47 | + ) => { |
| 48 | + const parentOnSelect = useContext(SelectableContext); |
| 49 | + const tabContext = useContext(TabContext); |
| 50 | + |
| 51 | + let getControlledId, getControllerId; |
| 52 | + |
| 53 | + if (tabContext) { |
| 54 | + activeKey = tabContext.activeKey; |
| 55 | + getControlledId = tabContext.getControlledId; |
| 56 | + getControllerId = tabContext.getControllerId; |
121 | 57 | }
|
122 | 58 |
|
| 59 | + const [needsRefocus, setRefocus] = useState(false); |
| 60 | + |
| 61 | + const listNode = useRef(null); |
| 62 | + |
| 63 | + const getNextActiveChild = offset => { |
| 64 | + if (!listNode.current) return null; |
| 65 | + |
| 66 | + let items = qsa(listNode.current, '[data-rb-event-key]:not(.disabled)'); |
| 67 | + let activeChild = listNode.current.querySelector('.active'); |
| 68 | + |
| 69 | + let index = items.indexOf(activeChild); |
| 70 | + if (index === -1) return null; |
| 71 | + |
| 72 | + let nextIndex = index + offset; |
| 73 | + if (nextIndex >= items.length) nextIndex = 0; |
| 74 | + if (nextIndex < 0) nextIndex = items.length - 1; |
| 75 | + return items[nextIndex]; |
| 76 | + }; |
| 77 | + |
| 78 | + const handleSelect = (key, event) => { |
| 79 | + if (key == null) return; |
| 80 | + if (onSelect) onSelect(key, event); |
| 81 | + if (parentOnSelect) parentOnSelect(key, event); |
| 82 | + }; |
| 83 | + |
| 84 | + const handleKeyDown = event => { |
| 85 | + if (onKeyDown) onKeyDown(event); |
| 86 | + |
| 87 | + let nextActiveChild; |
| 88 | + switch (event.key) { |
| 89 | + case 'ArrowLeft': |
| 90 | + case 'ArrowUp': |
| 91 | + nextActiveChild = getNextActiveChild(-1); |
| 92 | + break; |
| 93 | + case 'ArrowRight': |
| 94 | + case 'ArrowDown': |
| 95 | + nextActiveChild = getNextActiveChild(1); |
| 96 | + break; |
| 97 | + default: |
| 98 | + return; |
| 99 | + } |
| 100 | + if (!nextActiveChild) return; |
| 101 | + |
| 102 | + event.preventDefault(); |
| 103 | + handleSelect(nextActiveChild.dataset.rbEventKey, event); |
| 104 | + setRefocus(true); |
| 105 | + }; |
| 106 | + |
| 107 | + useEffect(() => { |
| 108 | + if (listNode.current && needsRefocus) { |
| 109 | + let activeChild = listNode.current.querySelector( |
| 110 | + '[data-rb-event-key].active', |
| 111 | + ); |
| 112 | + |
| 113 | + if (activeChild) activeChild.focus(); |
| 114 | + } |
| 115 | + }, [listNode, needsRefocus]); |
| 116 | + |
| 117 | + const mergedRef = useMergedRefs(ref, listNode); |
| 118 | + |
123 | 119 | return (
|
124 |
| - <SelectableContext.Provider value={this.handleSelect}> |
125 |
| - <NavContext.Provider value={this.state.navContext}> |
126 |
| - <Component |
127 |
| - {...props} |
128 |
| - onKeyDown={this.handleKeyDown} |
129 |
| - ref={this.attachRef} |
130 |
| - /> |
| 120 | + <SelectableContext.Provider value={handleSelect}> |
| 121 | + <NavContext.Provider |
| 122 | + value={{ |
| 123 | + role, // used by NavLink to determine it's role |
| 124 | + activeKey: makeEventKey(activeKey), |
| 125 | + getControlledId: getControlledId || noop, |
| 126 | + getControllerId: getControllerId || noop, |
| 127 | + }} |
| 128 | + > |
| 129 | + <Component {...props} onKeyDown={handleKeyDown} ref={mergedRef} /> |
131 | 130 | </NavContext.Provider>
|
132 | 131 | </SelectableContext.Provider>
|
133 | 132 | );
|
134 |
| - } |
135 |
| -} |
136 |
| - |
137 |
| -export default mapContextToProps( |
138 |
| - [SelectableContext, TabContext], |
139 |
| - (parentOnSelect, tabContext, { role }) => { |
140 |
| - if (!tabContext) return { parentOnSelect }; |
141 |
| - |
142 |
| - const { activeKey, getControllerId, getControlledId } = tabContext; |
143 |
| - return { |
144 |
| - activeKey, |
145 |
| - parentOnSelect, |
146 |
| - role: role || 'tablist', |
147 |
| - // pass these two through to avoid having to listen to |
148 |
| - // both Tab and Nav contexts in NavLink |
149 |
| - getControllerId, |
150 |
| - getControlledId, |
151 |
| - }; |
152 | 133 | },
|
153 |
| - AbstractNav, |
154 | 134 | );
|
| 135 | + |
| 136 | +AbstractNav.propTypes = propTypes; |
| 137 | +AbstractNav.defaultProps = defaultProps; |
| 138 | + |
| 139 | +export default AbstractNav; |
0 commit comments