Roving tabindex w/ React - javascript

What is most simple way to make "roving tabindex" in React? It's basically switch focus and tabindex=0/-1 between child elements. Only a single element have tabindex of 0, while other receives -1. Arrow keys switch tabindex between child elements, and focus it.
For now, I do a simple children mapping of required type, and set index prop and get ref, to use it later. It looks robust, but may be there more simple solution?
My current solution (pseudo-javascript, for idea illustration only):
ElementWithFocusManagement.js
function recursivelyMapElementsOfType(children, isRequiredType, getProps) {
return Children.map(children, function(child) {
if (isValidElement(child) === false) {return child;}
if (isRequiredType(child)) {
return cloneElement(
child,
// Return new props
// {
// index, iterated in getProps closure
// focusRef, saved to `this.focusable` aswell, w/ index above
// }
getProps()
);
}
if (child.props.children) {
return cloneElement(child, {
children: recursivelyMapElementsOfType(child.props.children, isRequiredType, getProps)
});
}
return child;
});
}
export class ElementWithFocusManagement {
constructor(props) {
super(props);
// Map of all refs, that should receive focus
// {
// 0: {current: HTMLElement}
// ...
// }
this.focusable = {};
this.state = {
lastInteractionIndex: 0
};
}
handleKeyDown() {
// Handle arrow keys,
// check that element index in `this.focusable`
// update state if it is
// focus element
}
render() {
return (
<div onKeyDown={this.handleKeyDown}>
<Provider value={{lastInteractionIndex: this.state.lastInteractionIndex}}>
{recursivelyMapElementsOfType(
children,
isRequiredType, // Check for required `displayName` match
getProps(this.focusable) // Get index, and pass ref, that would be saved to `this.focusable[index]`
)}
</Provider>
</div>
);
}
}
with-focus.js
export function withFocus(WrappedComponent) {
function Focus({index, focusRef, ...props}) {
return (
<Consumer>
{({lastInteractionIndex}) => (
<WrappedComponent
{...props}
elementRef={focusRef}
tabIndex={lastInteractionIndex === index ? 0 : -1}
/>
)}
</Consumer>
);
}
// We will match for this name later
Focus.displayName = `WithFocus(${WrappedComponent.name})`;
return Focus;
}
Anything.js
const FooWithFocus = withFocus(Foo);
<ElementWithFocusManagement> // Like toolbar, dropdown menu and etc.
<FooWithFocus>Hi there</FooWithFocus> // Button, menu item and etc.
<AnythingThatPreventSimpleMapping>
<FooWithFocus>How it's going?</FooWithFocus>
</AnythingThatPreventSimpleMapping>
<SomethingWithoutFocus />
</ElementWithFocusManagement>

react-roving-tabindex looks quite good.

Related

Rerender only specific Child Components in JSX map function

I am mapping through an array, which returns JSX Components for each of the items in the array. During runtime I want to pass down values. If they match the value of the individual items, their individual component gets modified.
I am trying to find a way to achieve this without rerendering all components, which currently happens because the props change
I have tried using shouldComponentUpdate in a class component, but it seems this way I can only compare prevState and prevProps with the corresponding changes. I have further considered useMemo in the Map function, which didnt work, because it was nested inside the map function.
const toParent=[1,2,4,5]
Parent Component:
function parent({ toParent }) {
const [myNumbers] = useState([1,2,3,4, ..., 1000]);
return (
<div>
{myNumbers.map((number, index) => (
<Child toChild = { toParent } number = { number }
index= { index } key = { number }/>
))}
</div>
)
}
Child Component:
function Child({toChild, number, index}){
const [result, setResult] = useState(() => { return number*index }
useEffect(()=> {
if (toChild.includes(number)) {
let offset = 10
setResult((prev)=> { return { prev+offset }})
}
}, [toChild])
return (
<div style={{width: result}}> Generic Div </div> )
}
The solution to my problem was using the React.memo HOC and comparing the properties to one another and exporting it as React.memo(Child, propsAreEqual).
Performance
This way other methods like findElementbyId (not recommended in any case) and shouldComponentUpdate to target specific items in a map function can be avoided.
Performance is quite good, too. Using this method cut down the rendering time from 40ms every 250ms to about 2 ms.
Implementation
In Child Component:
function Child(){...}
function propsAreEqual(prev, next) {
//returning false will update component, note here that nextKey.number never changes.
//It is only constantly passed by props
return !next.toChild.includes(next.number)
}
export default React.memo(Child, propsAreEqual);
or alternatively, if other statements should be checked as well:
function Child(){...}
function propsAreEqual(prev, next) {
if (next.toChild.includes(next.number)) { return false }
else if ( next.anotherProperty === next.someStaticProperty ) { return false }
else { return true }
}
export default React.memo(Key, propsAreEqual);

Close a dropdown when an element within it is clicked

I'm working on a Notification feature in my app (pretty much like Facebook notifications).
When I click a button in the header navigation, the dropdown opens and shows the notification list. The notification has a Link (from react-router) in it.
What I need to do is to close the dropdown whenever a Link is clicked.
Here's roughly the hierarchy I currently have:
Header > Navigation > Button > Dropdown > List > Notification > Link
Since the dropdown functionality is used more that once, I've abstracted its behavior away into a HOC that uses render prop:
export default function withDropDown(ClickableElement) {
return class ClickableDropdown extends PureComponent {
static propTypes = {
children: PropTypes.func.isRequired,
showOnInit: PropTypes.bool,
};
static defaultProps = {
showOnInit: false,
};
state = {
show: !!this.props.showOnInit,
};
domRef = createRef();
componentDidMount() {
document.addEventListener('mousedown', this.handleGlobalClick);
}
toggle = show => {
this.setState({ show });
};
handleClick = () => this.toggle(true);
handleGlobalClick = event => {
if (this.domRef.current && !this.domRef.current.contains(event.target)) {
this.toggle(false);
}
};
render() {
const { children, ...props } = this.props;
return (
<Fragment>
<ClickableElement {...props} onClick={this.handleClick} />
{this.state.show && children(this.domRef)}
</Fragment>
);
}
};
}
The HOC above encloses the Button component, so I have:
const ButtonWithDropdown = withDropdown(Button);
class NotificationsHeaderDropdown extends PureComponent {
static propTypes = {
data: PropTypes.arrayOf(notification),
load: PropTypes.func,
};
static defaultProps = {
data: [],
load: () => {},
};
componentDidMount() {
this.props.load();
}
renderDropdown = ref => (
<Dropdown ref={ref}>
{data.length > 0 && <List items={this.props.data} />}
{data.length === 0 && <EmptyList />}
</Dropdown>
);
render() {
return (
<ButtonWithDropdown count={this.props.data.length}>
{this.renderDropdown}
</ButtonWithDropdown>
);
}
}
List and Notification are both dumb functional components, so I'm not posting their code here. Dropdown is pretty much the same, with the difference it uses ref forwarding.
What I really need is to call that .toggle() method from ClickableDropdown created by the HOC to be called whenever I click on a Link on the list.
Is there any way of doing this without passing that .toggle() method down the Button > Dropdown > List > Notification > Link subtree?
I'm using redux, but I'm not sure this is the kind of thing I'd put on the store.
Or should I handle this imperatively using the DOM API, by changing the implementation of handleGlobalClick from ClickableDropdown?
Edit:
I'm trying with the imperative approach, so I've changed the handleGlobalClick method:
const DISMISS_KEY = 'dropdown';
function contains(current, element) {
if (!current) {
return false;
}
return current.contains(element);
}
function isDismisser(dismissKey, current, element) {
if (!element || !contains(current, element)) {
return false;
}
const shouldDismiss = element.dataset.dismiss === dismissKey;
return shouldDismiss || isDismisser(dismissKey, current, element.parentNode);
}
// Then...
handleGlobalClick = event => {
const containsEventTarget = contains(this.domRef.current, event.target);
const shouldDismiss = isDismisser(
DISMISS_KEY,
this.domRef.current,
event.target
);
if (!containsEventTarget || shouldDismiss) {
this.toggle(false);
}
return true;
};
Then I changed the Link to include a data-dismiss property:
<Link
to={url}
data-dismiss="dropdown"
>
...
</Link>
Now the dropdown is closed, but I'm not redirected to the provided url anymore.
I tried to defer the execution of this.toggle(false) using requestAnimationFrame and setTimeout, but it didn't work either.
Solution:
Based on the answer by #streletss bellow, I came up with the following solution:
In order to be as generic as possible, I created a shouldHideOnUpdate prop in the ClickableDropdown dropdown component, whose Hindley-Milner-ish signature is:
shouldHideOnUpdate :: Props curr, Props prev => (curr, prev) -> Boolean
Here's the componentDidUpdate implementation:
componentDidUpdate(prevProps) {
if (this.props.shouldHideOnUpdate(this.props, prevProps)) {
this.toggle(false);
}
}
This way, I didn't need to use the withRouter HOC directly in my withDropdown HOC.
So, I lifted the responsibility of defining the condition for hiding the dropdown to the caller, which is my case is the Navigation component, where I did something like this:
const container = compose(withRouter, withDropdown);
const ButtonWithDropdown = container(Button);
function routeStateHasChanged(currentProps, prevProps) {
return currentProps.location.state !== prevProps.location.state;
}
// ... then
render() {
<ButtonWithDropdown shouldHideOnUpdate={routeStateHasChanged}>
{this.renderDropdown}
</ButtonWithDropdown>
}
It seems you could simply make use of withRouter HOC and check if this.props.location.pathname has changed when componentDidUpdate:
export default function withDropDown(ClickableElement) {
class ClickableDropdown extends Component {
// ...
componentDidUpdate(prevProps) {
if (this.props.location.pathname !== prevProps.location.pathname) {
this.toggle(false);
}
}
// ...
};
return withRouter(ClickableDropdown)
}
Is there any way of doing this without passing that .toggle() method down the Button > Dropdown > List > Notification > Link subtree?
In the question, you mention that you are using redux.So I assume that you store showOnInit in redux.We don't usually store a function in redux.In toggle function,I think you should dispatch an CHANGE_SHOW action to change the showOnInit in redux, then pass the show data not the function to the children component.Then after reducer dispatch,the react will change “show” automatically.
switch (action.type) {
case CHANGE_SHOW:
return Object.assign({}, state, {
showOnInit: action.text
})
...
default:
return state
}
Link element and data pass
Use the property in Link-to,not data-...Like this:
<Link
to={{
pathname: url,
state:{dismiss:"dropdown"}
}}
/>
And the state property will be found in this.props.location.
give context a little try(not recommend)
It may lead your project to instable and some other problems.(https://reactjs.org/docs/context.html#classcontexttype)
First,define context
const MyContext = React.createContext(defaultValue);
Second,define pass value
<MyContext.Provider value={this.toggle}>
Then,get the value in the nested component
<div value={this.context} />

React Classes: Referencing class as "this", within an object's function property

I have finally gotten into using react and ES6 and it's going well but I am finally stumped and could use some direction.
I have got my head around binding this to a method to reference the class, but I am trying to go a bit deeper. Take this for example...which works as expected:
class App extends Component {
state = {
myFirstState: false,
};
handleMyFirstState = () => {
this.setState( { myFirstState : true } );
};
render() {
return (
<MyComponent handleMySate={ this.handleMyState } />
);
}
}
export default App;
As the amount of methods increased I decided NOT to pass each method individually as props and to group them in an object first, and to just pass the object as a whole, as a prop. Like So...
class App extends Component {
state = {
myFirstState: false,
mySecondState: false
};
handleMyFirstState = () => {
this.setState( { myFirstState : true } );
};
handleMySecondSate = () => {
this.setState( { mySecondState : true } );
};
render() {
const handleStates = {
first : this.handleMyFirstState,
second : this.handleMySecondState
}
return (
<MyComponent handleStates={ handleStates } />
);
}
}
export default App;
Now, I am trying to avoid redundant code and just build the methods as one object with functions as properties before the render begins. Pretty much like this...
class App extends Component {
state = {
myFirstState: false,
mySecondState: false
};
handleStates = {
// Here is where 'this' does not reference the App class
// I get results from the console log but setstate doesn't pass correctly
first : () => { console.log("First Triggered"); this.setState( { myFirstState : true } ); },
second : () => { console.log("Second Triggered"); this.setState( { mySecondState : true } ); }
};
render() {
return (
<MyComponent handleStates={this.handleStates} />
);
}
}
export default App;
// I trigger the function like this within MyComponent and I get the console log, but `this.setState` breaks.
<Button onClick={ this.props.handleState.first } >Handle First</button>
I have successfully triggered the functions from the child component ,<MyComponent/>, using the latter code, but this no longer refers to the class and I can't figure out how to bind this to handleStates since it's not a function.
Is this just not possible or is there another way to handle what I am trying to achieve?
Thank you in advance!
ADDITIONAL
If I move the handleStates into the render() it works just fine...how could that be?
class App extends Component {
state = {
myFirstState: false,
mySecondState: false
};
render() {
const handleStates = {
first : () => { this.setState( { myFirstState : true } ); },
second : () => { this.setState( { mySecondState : true } ); }
};
return (
<MyComponent handleStates={this.handleStates} />
);
}
}
export default App;
First, in the second example, you pass this.handleStates as the value for the prop handleStates, but it's undefined. You built handleStates as a local variable, and thus you want your props to reference that local variable:
<MyComponent handleStates={handleStates} />
For your third (last) example, your issue is even simpler: you defined handleStates as an attribute on this which is assigned an object, itself with two attributes, first and second, each of which have a function as their value.
When you ultimately pass this.handleStates to MyComponent, you're passing an object, not a function. If you want to call one of first or second from MyComponent, you can do so like this:
this.props.handleStates.first()
Which has the desired result of updating the state in App.
For what it's worth, there's a more common pattern for this: simply pass a single updater function as the prop, named according to what it does:
class Sandwich extends React.Component {
this.state = {
bread: "",
meat: "",
veggie: "",
}
updateSandwich = (component, selection) => {
this.setState({ [component]: selection })
}
render() {
return(<IngredientSelector updateSandwich={this.updateSandwich} />)
}
}
class IngredientSelector extends React.Component {
return(){
<button value="Rye" onClick={() => this.updateSandwich("bread", "rye")} />
<button value="Wheat" onClick={() => this.updateSandwich("bread", "wheat")} />
<button value="Ham" onClick={() => this.updateSandwich("meat", "ham")} />
<button value="Turkey" onClick={() => this.updateSandwich("meat", "turkey")} />
}
}

React this.props don't update

Can someone help me to understand why this.props doesn't update after i filter it?
Here the slim version of my code
export default class AutoList extends React.Component {
constructor(props) {
super(props);
this.state = {
filterValue: 'all',
isHidden: true,
autoOptValue: ''
}
}
handleOnChangeBrand(evt) {
let selectedValue = evt.target.value;
this.setState({optionValue: selectedValue});
let filtered = this.props.autos.filter((auto) => {
if(auto.brands){
return auto.brands[0] === selectedValue;
}
return false;
});
console.log(this.props.auto) // still same number
console.log(filtered) // less autos. Actual filtered array
}
render() {
let autoDetail = this.props.autos.map(auto => {
return (
<Auto
key={auto.id}
id={auto.id}
name={auto.name}
brands={auto.brands ? auto.brands : false}/>
);
});
return (
<div>
<section>
<select id='autoFilter' className={this.state.isHidden ? 'u-is-hidden' : ''} onChange={this.handleOnChangeBrand.bind(this)} value={this.state.autoOptValue}>
<option value='brand1'> brand1 </option>
<option value='brand2'> brand2 </option>
</select>
</section>
<ul>
{autoDetail}
</ul>
</div>
);
}
So basically i have this.prop.auto is an array of 100 auto, each of them is an object with brand (which is another array) with 2,3 brands each.
I was able to filter, since filtered give me back an array with filtered autos, the correct ones.
But after that, this.props.auto doesn't update, nor does the UI.
I did something similar but sorting the auto by the brands and it works smoothly.
I don't get the difference here
this.props is effectively immutable within a component, so you cannot update the value of this.props.autos. Array#filter is also a pure function, so the array being filtered is not altered, but a new filtered array is returned. This is why when you log filtered in your function you see the filtered array, but this.props.autos is unchanged.
The simple answer to this is to do the filtering within your render method - I have added an initial state for optionValue of false, and within the filter method checked for this and not filtered if it is still false.
export default class AutoList extends React.Component {
constructor(props) {
super(props);
this.state = {
filterValue: 'all',
isHidden: true,
autoOptValue: '',
optionValue: false
}
}
handleOnChangeBrand(evt) {
let selectedValue = evt.target.value;
this.setState({optionValue: selectedValue});
}
render() {
const { optionValue } = this.state;
const autoDetail = this.props.autos
.filter((auto) => {
if (!optionValue) return true;
if(auto.brands){
return auto.brands[0] === optionValue;
}
return false;
})
.map(auto => {
return (
<Auto
key={auto.id}
id={auto.id}
name={auto.name}
brands={auto.brands ? auto.brands : false}/>
);
});
return (
<div>
<section>
<select id='autoFilter' className={this.state.isHidden ? 'u-is-hidden' : ''} onChange={this.handleOnChangeBrand.bind(this)} value={this.state.autoOptValue}>
<option value='brand1'> brand1 </option>
<option value='brand2'> brand2 </option>
</select>
</section>
<ul>
{autoDetail}
</ul>
</div>
);
}
Filter returns a new array. This is so you still have the original on hand. There are so very, very many reasons this is a good thing, and practically 0 reasons it's a bad thing.
Props is meant to be immutable (just like arrays that you call filter on). Don't change the members on props. Nor should you change the members of the members of props, or any descendant data of props in general.
That is why state exists, so if you absolutely must, you can save it there.
Regarding #2, typically you should be pulling that stuff out into higher and higher layers of abstraction, getting away from the view data, completely, and just passing in finished data that's ready for showing.
The Array.prototype.filter() method always returns a new filtered array without changing the old one:
The filter() method creates a new array with all elements that pass the test implemented by the provided function.
In your handleOnChangeBrand event, you're creating a filtered array, without affecting the old one, but then not using that filtered array when React calls render for the second time.
A small example of how you could handle this would be as follows:
1) Have react render your default autos prop
export default class AutoList extends React.Component {
render() {
const autoDetail = this.props.autos.map(auto => {
return (
<Autos ... />
)
});
return (
<ul>
{ autoDetail }
</ul>
);
}
}
2) Add in a click handler and a state value to hold the value what you would like to filter by:
export default class AutoList extends React.Component {
constructor(props) {
super(props);
this.state = {
filter: '' // this is where we will store what we want to filter by
}
};
// All this function will do is update the state which we want to filter by
// this 'arrow function' auto bind 'this' for us, so we don't have to explicitely set .bind as you were doing before
handleOnChangeBrand = (event) => {
this.setState({
filter: event.target.value
})
};
render() {
const autoDetail = this.props.autos.map(auto => {
return (
<Autos ... />
)
});
return (
<ul>
{ autoDetail }
</ul>
);
}
}
3) Finally, we will use the value we are storing in state to filter to the auto brands we would like and use that to build our array
export default class AutoList extends React.Component {
constructor(props) {
super(props);
this.state = {
filter: '' // this is where we will store what we want to filter by
}
};
getFilteredAutos = () => {
// if we have no filter, return everything!
if (this.state.filter === '') {
return this.props.autos;
}
// this is returning the newely filtered array, without affecting the old one
return this.props.autos.filter(auto => {
if(auto.brands){
// we're filtering by our saved value
return auto.brands[0] === this.state.filter;
}
return false;
});
},
handleOnChangeBrand = (event) => {
this.setState({
filter: event.target.value
})
};
render() {
// we're mapping by the filtered results here
const autoDetail = this.getFilteredAutos().map(auto => {
return (
<Autos ... />
)
});
return (
<ul>
{ autoDetail }
</ul>
);
}
}

React: Binding array to dynamically added fields

Lets say I have a class with a state level array
ElementsClass = React.createClass({
getInitialState: function() {
return {
elements: []
}
},
addElement: function() {
var element = {
name: ""
};
},
render() {
return (
{this.state.elements.map(function (element, i) {
return <input value={element.name} />
}
)}
)
}
The idea being that I can dynamically add to the elements array and have a new input field appearing.
How do I bind the data so that I am able to change the value in the input field and have that reflect automatically in the correct element in the elements array?
To dynamically sync your inputs with your state array you can use someting called linkState from the react-catalyst package. Once you've installed it with npm you can use it in the following way:
//need to import
import Catalyst from 'react-catalyst';
ElementsClass = React.createClass({
// mixin the linkedstate component
mixins : [Catalyst.LinkedStateMixin],
getInitialState: function() {
return {
elements: []
}
},
addElement: function() {
var element = {
name: ""
};
//add to elements array
this.state.elements.push(element);
//let react know to rerender necessary parts
this.setState({
elements : this.state.elements
});
},
render() {
return (
{this.state.elements.map(function (element, i) {
//use the linkState method
return <input valueLink={this.linkState('elements.'+i+'.name')} />
}
)}
)
}
The reason we need the react-catalyst package is that natively React's valueLink will only link top level state items, in your case elements. Obviously this isn't particularily useful but thankfully it's a fairly easy problem to solve.
Note: for iterated items like your element inputs, you need to provide a unique key. Something like the following (might need modifying to be more specific):
{this.state.elements.map(function (element, i) {
//use the linkState method
return <input valueLink={this.linkState('elements.'+i+'.name')} key={'elinput' + i} />
}
)}
This doesn't have any outward effect on your app, it's mostly to help react target the element internally.
If you want to do this with just ES5 and React, one solution would be this:
var ElementsClass = React.createClass({
getInitialState: function() {
return {
elements: []
}
},
createElement: function(){
var element = {
name: ''
};
this.setState({elements: this.state.elements.concat(element)});
},
updateElement: function(pos, event) {
var value = event.target.value;
var updatedElements = this.state.elements.map(function(element, i){
if (i === pos){
return {name: value};
}
return element;
});
this.setState({elements: updatedElements});
},
render: function() {
console.log(this.state.elements);
return (
<div>
{this.state.elements.map(function (element, i) {
var boundClick = this.updateElement.bind(this, i);
return <input key={i} onKeyUp={boundClick}/>
}.bind(this))}
<button onClick={this.createElement}>Add Element</button>
</div>
)
}
});
React.render(<ElementsClass />, document.getElementById('app'));
You want to treat component state as immutable, so you don't want to call a mutating method like push on elements.
These situations are handled easily with custom links packages.
State and Forms in React, Part 3: Handling the Complex State
import Link from 'valuelink';
// linked inputs will be deprecated, thus we need to use custom wrappers
import { Input } from 'valueLink/tags.jsx'
const ElementsClass = React.createClass({
getInitialState: function() {
return {
elements: []
}
},
render() {
// Take link to the element
const elementsLink = Link.state( this, 'elements' );
return (
<div>
{ elementsLink.map( ( elementLink, i ) => (
<Input key={ i } valueLink={ elementLink.at( 'name' ) } />
))}
<button onClick={ elementsLink.push({ name : '' })}>
Add Elements
</button>
</div>
);
}
});

Categories

Resources