rafgraph / React Interactive
Programming Languages
Projects that are alternatives of or similar to React Interactive
React Interactive v0
Demo website (code in the demo repo)
Key features:
- Style touch interactions in web apps to look like native apps
- Style keyboad interactions separate from mouse and touch interactions (focus from tab key, etc)
- Makes every Interactive div/span/etc accessible by default (tab index, role and key click handler added)
- Use inline styles for all interactive states -
hover
,active
,focus
, etc... (no style tags or CSS added to the page), or use class names if you prefer to write styles separately with CSS - State change hook to easily incorporate the interactive state into your component (not possible with CSS)
- Separate
active
states for mouse, touch, and keyboard interactions (not possible with CSS) - Separate
focus
states based on how it was entered - from mouse, touch, or tab key (not possible with CSS) - Easily style and show/hide children based on the
Interactive
parent's state (only possible with complex CSS selectors) - Built in touch device and keyboard support - a
click
event is generated on mouse click, touch tap (without delay), and enter keydown
import Interactive from 'react-interactive';
...
<Interactive
as="div" // what the Interactive component is rendered as, can be anything
hover={{ color: 'green' }} // style object, can use any styles you'd like
active={{ color: 'blue' }}
// OR
hoverActive={{ color: 'red' }}
touchActive={{ color: 'blue' }}
keyActive={{ color: 'orange' }}
focus={{ outline: '2px solid green' }}
// OR
focusFromTab={{ outline: '2px solid orange' }}
focusFromMouse={{ outline: '2px solid green' }}
focusFromTouch={{ outline: '2px solid blue' }}
// hook called on every state change, receives prevState and nextState objects
onStateChange={this.handleInteractiveStateChange}
onClick={this.handleClick}
style={{ fontSize: '16px', padding: '3px', color: 'black' }}
>This is an interactive and focusable div</Interactive>
Table of Contents
- React Interactive
- The Basics
- API
- Interactive State Machine Comparison
- State Machine Notes
- More Examples
The Basics
Interactive State Machine
Interactive state machine as a React component. There are 5 mutually exclusive iStates, plus 3 mutually exclusive focus states that can be combined with the 5 iStates (the total number of states that RI can be in is 19, see State Machine Notes below).
- The 5 mutually exclusive iStates are:
normal
hover
- *
hoverActive
- *
touchActive
- *
keyActive
- The 3 mutually exclusive focus states are:
- **
focusFromTab
- **
focusFromMouse
- **
focusFromTouch
- **
*The 3 separate [type]Active
states can be treated as a single active
state if desired. hoverActive
(mouse on and button down), touchActive
(touch on screen), keyActive
(has focus and enter key down).
**The 3 separate focusFrom[Type]
states can be treated as a single focus
state if desired.
Compared to CSS, React Interactive is a simpler state machine with better touch device and keyboard support, and state change hooks. See comparison below.
Basic Examples
// Interactive div with state change hook
<Interactive
as="div"
normal={{ color: 'black' }}
hover={{ color: 'green' }}
active="hover" // use the hover state style for the active state
style={{ fontSize: '16px', padding: '3px', border: '2px dotted black' }}
onClick={this.handleClick}
onStateChange={this.handleInteractiveStateChange}
>This is an interactive div with state change hook</Interactive>
// Interactive as a React Router Link component
import { Link } from 'react-router-dom';
...
<Interactive
as={Link}
to="/some/path"
hover={{ color: 'green' }}
active={{ color: 'blue' }}
style={{ color: 'black', padding: '3px' }}
>This is an interactive React Router Link component</Interactive>
// Interactive link with separate styles for mouse, touch, and keyboard interactions
<Interactive
as="a"
href="https://example.tld"
normal={{ color: 'black' }}
// mouse interactions: normal -> hover -> hoverActive
hover={{ color: 'green' }}
hoverActive={{ color: 'red' }}
// touch interactions: normal -> touchActive
touchActive={{ color: 'blue' }}
// keyboard interactions: normal -> normal with focusFromTab -> keyActive with focusFromTab
focusFromTab={{ outline: '2px solid orange' }}
keyActive={{ color: 'orange' }}
>This is an interactive link with separate styles for each type of interaction</Interactive>
// Interactive div with class names instead of styles
<Interactive
as="div"
hover={{ className: 'hover-class' }}
hoverActive={{ className: 'hover-active-class' }}
touchActive={{ className: 'touch-active-class' }}
keyActive={{ className: 'key-active-class' }}
// use focusFromTab to only apply the class when focus comes from the keyboard
focusFromTab={{ className: 'tab-focus-class' }}
className="some-class"
>This is an interactive div with classes instead of inline styles</Interactive>
react-interactive
Installing $ yarn add react-interactive
# OR
$ npm install --save react-interactive
import Interactive from 'react-interactive';
// OR
var Interactive = require('react-interactive');
Or use the UMD build that's available on Unpkg (the component will be available to use as Interactive
)
<script src="https://unpkg.com/react-interactive/dist/ReactInteractive.min.js"></script>
API
<Interactive />
API for Note that there are no default values for any prop, and the only required prop is as
.
For the definition of when each state is entered, see the state machine definition below.
Prop | Type | Example | Description |
---|---|---|---|
as |
string (html tag name) or ReactComponent or JSX/ReactElement |
"div" or MyComponent or <div>...</div> <MyComponent />
|
What the Interactive component is rendered as. It can be an html tag name (as a string), or it can be a ReactComponent (RI's callbacks are passed down as props to the component), or it can be a JSX/ReactElement (see as prop type notes for more info). Note that as is hot-swappable on each render and RI will seamlessly maintain the current interactive state. The as prop is required (it is the only required prop). |
normal |
style object or options object or string |
{ color: 'black' } or { style: { color: 'black' }, className: 'some-class' } or 'hover'
|
Style or options object for the normal state, or a string indicating a state to match. If it's an object, it can be either a style object or an options object with the keys style and className . The style object is merged with both the style prop and the focus state style (see merging styles for the order that styles are merged in). The className is a string of space separated class names and is merged as a union with the className prop and the focus state className . If the value of the normal prop is a string, it must indicate one of the other states, e.g. 'hover' , and that state's style and className properties will be used for both states. |
hover |
style object or options object or string |
{ color: 'green' } or... (same as above) |
Same as normal , but for the hover state. Note that if there is no hoverActive or active prop, then the hover prop's style and classes are used for the hoverActive state. This state is entered when the mouse is on the RI element. |
active |
style object or options object or string |
{ color: 'red' } or... (same as above) |
Same as normal , but for the active state. Note that the active state is the union of the hoverActive , touchActive , and keyActive states. The active prop is only used in place of the [type]Active prop if the respective [type]Active prop is not present. |
hoverActive |
style object or options object or string |
{ color: 'red' } or... (same as above) |
Same as normal , but for the hoverActive state. Note that if there is no hoverActive or active prop, then the hover prop's style and classes are used for the hoverActive state. This state is entered when the mouse is on the RI element and the mouse button is down. |
touchActive |
style object or options object or string |
{ color: 'blue' } or... (same as above) |
Same as normal , but for the touchActive state. This state is entered when a touch point is on the RI element. |
keyActive |
style object or options object or string |
{ color: 'yellow' } or... (same as above) |
Same as normal , but for the keyActive state. This state is entered when the RI element has focus and the enter key is down. |
focus |
style object or options object or string |
{ outline: '2px solid green' } or... (same as above) |
Same as normal , but for the focus state. Note that the focus state is the union of the focusFromTab , focusFromTouch , and focusFromMouse states. The focus prop is only used in place of the focusFrom[Type] prop if the respective focusFrom[Type] prop is not present. |
focusFromTab |
style object or options object or string |
{ outline: '2px solid green' } or... (same as above) |
Same as normal , but for the focusFromTab state. This state is entered if focus is from the tab key (i.e. tabbing through the focusable elements on the page). Also, any focus calls not from a mouse or touch interaction (e.g. from assistive tech) will match with focusFromTab . |
focusFromMouse |
style object or options object or string |
{ outline: '2px solid red' } or... (same as above) |
Same as normal , but for the focusFromMouse state. This state is entered when focus is from a mouse interaction. |
focusFromTouch |
style object or options object or string |
{ outline: '2px solid blue' } or... (same as above) |
Same as normal , but for the focusFromTouch state. This state is entered when focus is from a touch interaction. |
style |
style object | { margin: '10px' } |
Styles that are always applied. Styles are merged with state styles. State styles have priority when there are conflicts. |
className |
string | "some-class other-class" |
Classes that are always applied to the element, and are merged as a union with state classes. |
onStateChange |
function | function({ prevState, nextState, event }) {...} |
Function called on each state change. Receives an object with prevState , nextState and event keys as the sole argument. prevState and nextState are state objects. The event is the event that caused the state change (a synthetic React event). |
setStateCallback |
function | function({ prevState, nextState }) {...} |
Function passed in as a callback when RI calls setState . Receives the same object as onStateChange as its sole argument, except without the event key (setState is asynchronous and React events don't persist asynchronously). Use this hook if you need to wait until the DOM is updated before executing the callback. |
onClick |
function |
function(event, clickType) {...} Where clickType is one of: 'mouseClick' 'tapClick' 'keyClick'
|
Function called for mouse clicks, touch taps with 1 touch point/finger (called without delay), enter keydown events (if the element has focus), and synthetic click events. The event argument will always be a click event (node.click() is called to generate a click event if needed). The clickType argument will always be one of mouseClick , tapClick , or keyClick . It will be mouseClick for mouse clicks and for synthetic click events on mouse only and hybrid devices. It will be tapClick for touch taps with 1 touch point and for synthetic click events on touch only devices. It will be keyClick if the click event was generated from a enter keydown event (or, for some elements, a space keyup event). Note that RI will call node.click() for enter keydown events only if there is an onClick prop. |
onTapTwo |
function | function(event) {...} |
Function called for taps with 2 touch points, e.g. a 2 finger tap. Event passed in is the touchend event from last touch point to leave the surface. |
tapTimeCutoff |
whole number | 500 |
Number of ms to allow for a tap. This is the cutoff time that separates a tap from a long press. This prop is not required and the default is 500 . |
onLongPress |
function | function(event) {...} |
Function called on long press if touch is present after the tapTimeCutoff and if the touch has not moved more than is allowed for a tap . Event passed in is the touch start event that started the long press. |
touchActiveTapOnly |
boolean | touchActiveTapOnly |
Add this prop to only remain in the touchActive state while a tap is possible. If the touch is moved more than the tolerance for a tap, or held on the screen longer than the time allowed for a tap, then the touchActive state is exited. This is useful when the intention of the touchActive state is to indicate to the user that they are tapping something. Note that without this prop React Interactive will remain in the touchActive state as long as the touch point is on the screen. |
extraTouchNoTap |
boolean | extraTouchNoTap |
Add this prop to cancel taps while touching someplace else on the screen. By default RI will ignore extra touches on the screen and allow taps on the RI element regardless of other touches. |
nonContainedChild |
boolean | nonContainedChild |
Add this prop if the DOM node's children are not contained inside of it on the page. For example, a child that is absolutely positioned outside of its parent. React Interactive does some quality control checks using node.getBoundingClientRect() , and by default the children are assumed to be within the parent's rectangle, but if this is not the case, then add this prop and the children will be checked. |
initialState |
state object | { iState: 'normal', focus: 'tab' } |
Optional initial state to enter when the component is mounted. A state object with keys for one or both of iState and focus . Note that for an active iState , you must specify [type]Active and not just active . Used in the constructor to set iState and in componentDidMount to set focus (RI can't set focus until after it has a reference to the DOM node). |
forceState |
state object | { iState: 'normal', focus: false } |
Force enter this state. Same as initialState except not used for the initial render. Note that if only one key is present, a shallow merge is done with the current state, for example, use { focus: 'tab' } to only focus the element. Only used in componentWillReceiveProps . |
stylePriority |
object | { hover: true, hoverActive: true } |
By default the focus state style takes precedence over the iState style when merged (except for the keyActive iState). Use this prop to specify specific iStates whose style should take precedence over the focus state style. Note that for an active iState , you must specify [type]Active and not just active . |
refDOMNode |
function | function(node) {...} |
Function is passed in a reference to the DOM node, and is called whenever the node changes. You shouldn't need to use this for anything related to React Interactive, but it's available in case you need to use it for other things. Note that if you need to focus/blur the DOM node, use the forceState or initialState prop and set the focus state instead of calling focus/blur directly on the DOM node. |
focusToggleOff |
boolean | focusToggleOff |
Add this prop to prevent focus from toggling on mouseup/tap. With this prop RI will enter the focus state normally and will remain in the focus state until the browser sends a blur event. |
mutableProps |
boolean | mutableProps |
Add this prop if you are passing in mutable props so the component will always update. By default it's assumed that props passed in are immutable. A shallow compare is done, and if the props are the same, the component will not update. If you're not sure and notice buggy behavior, then add this prop. |
interactiveChild |
boolean | interactiveChild |
Add this prop if Interactive's children use the Interactive Children API. |
wrapperStyle |
style object | { display: 'block' } |
Styles that are applied to the span wrapper if as is a ReactComponent. |
wrapperClassName |
string | "ri-wrapper-class other-class" |
Classes that are applied to the span wrapper if as is a ReactComponent. |
... |
anything |
id="some-id" , tabIndex="1" , etc... |
All additional props received are passed through. |
Merging Styles and Classes
- Styles are merged in the following order (last one takes precedence):
- The
style
prop - The iState style (except
keyActive
) - The focus state style if in the focus state
- The
keyActive
state style
- The
- If you want an iState style to take precedence over the focus style, then use the
stylePriority
prop and specify which iStates should have priority over focus, e.g.stylePriority={{ hover: true, hoverActive: true }}
- Classes are merged as a union without preference:
- focus state classes if in the focus state
- iState classes
- The
className
prop
as
Prop Type
- If
as
is a string:- E.g.
as="div"
- The string must be an html tag name, for example,
div
,span
,a
,h1
,p
,ul
,li
,input
,select
, etc... - Note that for SVG images,
as="svg"
works fine except that in general SVGs are not focusable by the browser, so if you needfocus
then wrap thesvg
element in a Interactivespan
. Also with SVGs you can make a specific path interactive, e.g.as="path"
to create interactions within the SVG.
- E.g.
- If
as
is a ReactComponent:- E.g.
as={MyComponent}
- Strictly speaking this means that
as
is either a ReactClass or a ReactFunctionalComponent as defined in the React Glossary. - In order for React Interactive to work
as
a ReactComponent, the component must pass down the props it receives from React Interactive to the top DOM node that it renders, and it cannot override any of the passed down event handlers, e.g.onMouseEnter
. Also, the component cannot replace its top DOM node once it's rendered unless the replacement is the result of new props (note that mutations are okay, e.g. changing style, classes, children, etc is fine). This is because React Interactive keeps a reference to the component's top DOM node so it can do things like callfocus()
, and if the top DOM node is replaced without React Interactive's knowledge, then things start to break. Note that React Router's Link component meets these requirements. - When
as
is a ReactComponent it is wrapped in a<span>
in order for React Interactive to maintain a reference to the top DOM node without breaking encapsulation. Without the span wrapper the only way to access the top DOM node would be through usingReactDOM.findDOMNode(component)
, which breaks encapsulation and is discouraged, and also doesn't work with stateless functional components.- The
<span>
wrapper can be styled by passing down the propswrapperClassName
(class string) andwrapperStyle
(style object).
- The
- E.g.
- If
as
is a JSX/ReactElement:- E.g.
as={<div>...</div>}
oras={<MyComponent />}
- The props of the top ReactElement are merged with, and have priority over, Interactive's props. For example:
const jsxElement = <div hover={{ color: 'blue' }}>Some jsxElement text</div>; <Interactive as={jsxElement} hover={{ color: 'green' }} active={{ color: 'red' }} >Some other text</Interactive>
- This will create a
div
with text that reads 'Some jsxElement text' and will be blue on hover and red on active. When the props are merged,jsxElement
'shover
prop andchildren
have priority overInteractive
'shover
prop andchildren
, but sincejsxElement
didn't specify anactive
prop,Interactive
'sactive
prop is still valid. - After the props are merged, the JSX/ReactElement's type (html tag name or ReactComponent) determines how
as
is processed - either like a string or like a ReactComponent. - Note that when
as
is a ReactElement you cannot attach aref
to it (only theInteractive
element is rendered and you can attach aref
toInteractive
(or use therefDOMNode
prop), but it is not possible to have tworef
s to the same element). - Note that this is not a very practical example of using a JSX/ReactElement for
as
. For a more practical example see, Hot Swappableas
.
- E.g.
state
Object
- The React Interactive state object looks like this:
// this.state
{
// iState is always 1 of 5 strings
iState: 'normal' / 'hover' / 'hoverActive' / 'touchActive' / 'keyActive',
// focus is always 1 or 4 values
focus: false / 'tab' / 'mouse' / 'touch',
}
- In RI's API, the
onStateChange
andsetStateCallback
hooks receive the previous and next state objects when they are called, and theforceState
andinitialState
props pass in a state object to the RI component.
role
and tabIndex
Default - If you add an
onClick
prop without arole
prop, and it's not clear what the role of the element is (i.e. it's not for user input, a link, or an area tag), then RI will automatically addrole="button"
for better accessibility. If you don't want anyrole
added to the DOM element, then pass in the proprole={null}
. - If you add a
focus
oronClick
prop without atabIndex
prop, then atabIndex
of0
is added to make the element focusable by the browser. If you don't want anytabIndex
added to the DOM element, then pass in the proptabIndex={null}
. - Note that for buttons
as="button"
is discouraged because browsers are inconsistent in how they display and handle button interactions. For better consistency, useas="div"/"span"
and add anonClick
handler. By default RI will addrole="button"
,tabIndex="0"
, and a key click handler (which will callonClick
), so it will work just like a button. You can override these with your ownrole
andtabIndex
if you prefer.
Focus State
- The focus state can be applied to any element, not just inputs, and will toggle on click/tap unless the element is for user input.
- React Interactive's focus state is always kept in sync with the browser's focus state. Added functionality like focus toggle and
focusFrom
are implemented by controlling the browser's focus state. - Focus toggle
- All elements will toggle focus except if the element is for user input, that is, the element's tag name is
input
,textarea
, orselect
. - For mouse interactions, the focus state is entered on mouse down, and toggled off on mouse up providing it didn't enter the focus state on the preceding mouse down.
- For touch interactions, the focus state in entered with a 1 touch point/finger tap, and toggled off on the next 1 finger tap. Also, on touchOnly devices, a click event not preceded by a touch event (e.g. a synthetic click event) will toggle focus on/off.
- If you want to turn off focus toggle, then add the
focusToggleOff
prop. With this prop RI will enter the focus state normally and will remain in the focus state until the browser sends a blur event.
- All elements will toggle focus except if the element is for user input, that is, the element's tag name is
- The focus state change occurs in the same
setState
call as the iState change, so theonStateChange
hook is only called once. For example,onMouseDown
enters thefocus
state and thehoverActive
state in a single state change (and render). This achieved by controlling the browser's focus state - without this control the browser would fire the focus event immediately after the mouse down event resulting in twosetState
calls (and twoonStateChange
calls), one to enter thehoverActive
state and one to enter thefocus
state.
Default Styles
- If a
focus
prop is passed to React Interactive, then RI will prevent the browser's default focus outline from being applied. - If clicking the mouse does something, then the cursor will default to a pointer.
- If you want to use the browser default styles without any interference from RI, then add the below props:
useBrowserOutlineFocus
useBrowserCursor
- If a
touchActive
oractive
prop is passed to React Interactive, then RI will prevent the browser's default webkit tap highlight color from being applied.- To use the
WebkitTapHighlightColor
for styling, don't provide atouchActive
oractive
prop and set theWebkitTapHighlightColor
style in the mainstyle
prop. - Note that if there is no active or touchActive prop, RI will let the browser fully manage what it considers to be a click from a touch interaction. This results in a better match of when the
WebkitTapHighlightColor
is active to what results in a click. RI won't callnode.click()
, so there may be a delay in the click event in some browsers.
- To use the
Interactive Children API
- Note that you must add the
interactiveChild
prop to<Interactive />
to use the Interactive Children API (by default RI will not inspect its children and will render them as is). - If you have nested
Interactive
components, the children will be styled based on the state of their closestInteractive
parent.
function InteractiveChild() {
return (
<Interactive
as="ul"
interactiveChild // so Interactive will style the children based on its state
focusFromTab={{}} // so the Interactive component is focusable
touchActive={{}} // so Interactive will control taps and remove the browser's default style
>
<li>This list item will not change style based on the state of the Interactive parent.</li>
<li
onParentHover={{ color: 'green' }}
onParentHoverActive="hover" // use the onParentHover style for onParentHoverActive
onParentTouchActive={{ color: 'blue' }}
onParentFocusFromTab={{ outline: '2px solid green' }}
>
This list item will change style based on the state of the Interactive parent.
</li>
<li
showOnParent="hover hoverActive touchActive focusFromTab"
>
This list item is only rendered when the Interactive parent is in the
hover, hoverActive, touchActive or focusFromTab states.
</li>
</Interactive>
);
}
Prop | Type | Example | Description |
---|---|---|---|
showOnParent |
space separated string | 'hover touchActive focusFromTab' |
Add this props to only render the child when the parent is in any of the listed states. Without this prop, RI will always render the child. The acceptable state values are: hover , active (union of the 3 [type]Active states), hoverActive , touchActive , keyActive , focus (union of the 3 focusFrom[Type] states), focusFromTab , focusFromMouse , and focusFromTouch . List as a space separated string. |
onParentNormal |
style object or options object or string |
{ color: 'black' } or { style: { color: 'black' }, className: 'some-class' } or 'hover'
|
Style or options object when the parent is in the normal state, or a string indicating a state to match. If it's an object, it can be either a style object or an options object with the keys style and className . The style object is merged with both the child's style prop and the onParentFocusFrom[Type] style in the same order as the Interactive parent. The className is a string of space separated class names and is merged as a union with the child's className prop and the onParentFocusFrom[Type] className . If the value of the onParentNormal prop is a string, it must indicate one of the other states, e.g. 'hover' (without the onParent prefix), and that state's onParent[State] style and className properties will be used for both states. Note that the interface is the same as <Interactive /> 's normal prop. |
onParentHover |
style object or options object or string |
{ color: 'green' } or... (same as above) |
Same as onParentNormal , but for the parent's hover state. Note that if there is no onParentHoverActive or onParentActive prop, then the onParentHover prop's style and classes are used for the onParentHoverActive prop. |
onParentActive |
style object or options object or string |
{ color: 'red' } or... (same as above) |
Same as onParentNormal , but for the parent's active state. Note that the onParentActive prop is only used in place of the onParent[Type]Active prop if the respective onParent[Type]Active prop is not present. |
onParentHoverActive |
style object or options object or string |
{ color: 'red' } or... (same as above) |
Same as onParentNormal , but for the parent's hoverActive state. Note that if there is no onParentHoverActive or onParentActive prop, then the onParentHover prop's style and classes are used for the onParentHoverActive prop. |
onParentTouchActive |
style object or options object or string |
{ color: 'blue' } or... (same as above) |
Same as onParentNormal , but for the parent's touchActive state. |
onParentKeyActive |
style object or options object or string |
{ color: 'yellow' } or... (same as above) |
Same as onParentNormal , but for the parent's keyActive state. |
onParentFocus |
style object or options object or string |
{ outline: '2px solid green' } or... (same as above) |
Same as onParentNormal , but for the parent's focus state. Note that the onParentFocus prop is only used in place of the onParentFocusFrom[Type] prop if the respective onParentFocusFrom[Type] prop is not present. |
onParentFocusFromTab |
style object or options object or string |
{ outline: '2px solid green' } or... (same as above) |
Same as onParentNormal , but for the parent's focusFromTab state. |
onParentFocusFromMouse |
style object or options object or string |
{ outline: '2px solid red' } or... (same as above) |
Same as onParentNormal , but for the parent's focusFromMouse state. |
onParentFocusFromTouch |
style object or options object or string |
{ outline: '2px solid blue' } or... (same as above) |
Same as onParentNormal , but for the parent's focusFromTouch state. |
Interactive State Machine Comparison
Compared to CSS, React Interactive is a simpler state machine, with better touch device and keyboard support, and state change hooks.
- Let's define some basic mouse, touch, and keyboard states:
-
mouseOn
: the mouse is on the element -
buttonDown
: the mouse button is down while the mouse is on the element -
touchDown
: at least one touch point is in contact with the screen and started on the element -
focusKeyDown
:- For React Interactive, this is if:
- The element is not a checkbox, radio, or select, and the enter key is down
- The element is a button and the space bar or enter key is down
- The element is a checkbox, radio, or select and the space bar is down
- Convention is buttons are activated by both the space bar and enter key, and checkboxes, radio buttons and selects are only activated by the space bar
- For CSS, this is something like, if the element is a button, checkbox, or radio button, and the space bar is down, then it is in the active state (i.e. the
foucsKeyDown
state for the purposes of this abstraction), but is not consistent across browsers. Note that even though the enter key triggers links and buttons, pressing the enter key won't cause an element to enter the active state, which means that with CSS there is no way to give visual feedback when triggering an element with the enter key.
- For React Interactive, this is if:
-
React Interactive State Machine
Interactive state | Mouse, touch and keyboard states |
---|---|
base styles | Not an interactive state, always applied, everything merges with them |
normal |
!mouseOn && !buttonDown && !touchDown && !focusKeyDown |
hover |
mouseOn && !buttonDown && !touchDown && !focusKeyDown |
active |
hoverActive OR keyActive OR touchActive
|
hoverActive |
mouseOn && buttonDown && !touchDown && !focusKeyDown |
keyActive |
focusKeyDown && !touchDown |
touchActive |
touchDown |
The three focusFrom
states can be combined with any of the above states, and the keyActive
state is only available while in the focus state.
CSS Interactive State Machine
Note that since a state machine can only be in one state at a time, to view interactive CSS as a state machine it has to be thought of as a combination of pseudo class selectors that match based on the mouse, keyboard and touch states.
Interactive state | Note | Mouse, touch and keyboard states | CSS Selector(s) |
---|---|---|---|
base styles | Always applied, everything merges with them | Not an interactive state | .class |
normal |
Not commonly used in CSS (zeroing out/overriding base styles is used instead) | !mouseOn && !buttonDown && !touchDown && !focusKeyDown |
.class:not(:hover):not(:active) |
hover |
Only hover styles applied |
(mouseOn && !buttonDown && !focusKeyDown) OR (after touchDown and sticks until you tap someplace else) - the sticky hover CSS bug on touch devices |
.class:hover |
hoverActive |
Both hover and active styles applied |
(mouseOn && buttonDown) OR (mouseOn && focusKeyDown) OR (touchDown, but not consistent across browsers)
|
.class:hover , .class:active
|
active |
Only active styles applied |
(buttonDown && !mouseOn currently, but had mouseOn when buttonDown started) OR (focusKeyDown && !mouseOn) OR (touchDown but not on the element currently, but not consistent across browsers)
|
.class:active |
The focus state can be combined with any of the above CSS interactive states to double the total number of states that the CSS interactive state machine can be in.
Note that you could achieve mutually exclusive hover and active states if you apply hover styles with the .class:hover:not(:active)
selector, and there are other states that you could generate if you wanted to using CSS selectors. You could also create a touch active state by using Current Input, so CSS has some flexibility, but it comes at the cost of simplicity, and in CSS touch and keyboard interactions are not well supported.
State Machine Notes
- The total number of states that the React Interactive state machine can be in is 19.
- There are 5 mutually exclusive and comprehensive iStates:
normal
,hover
,hoverActive
,touchActive
, andkeyActive
. These are combined with 4 mutually exclusive and comprehensive focus states:false
,tab
,mouse
, andtouch
, with the exception ofkeyActive
, which is only available while focus is notfalse
, for a total of 19 states:
normal |
hover |
hoverActive |
touchActive |
N/A |
---|---|---|---|---|
normal with focusFromTab
|
hover with focusFromTab
|
hoverActive with focusFromTab
|
touchActive with focusFromTab
|
keyActive with focusFromTab
|
normal with focusFromMouse
|
hover with focusFromMouse
|
hoverActive with focusFromMouse
|
touchActive with focusFromMouse
|
keyActive with focusFromMouse
|
normal with focusFromTouch
|
hover with focusFromTouch
|
hoverActive with focusFromTouch
|
touchActive with focusFromTouch
|
keyActive with focusFromTouch
|
- The
onStateChange
hook is called each time a transition occurs between any of the 19 states. Note that a transition will never occur between twofocusFrom
states asfocusFrom
is based on how the focus state was entered, so have to transition to focusfalse
before transitioning to a differentfocusFrom
state. - The
active
prop is just a convenience wrapper around the 3 specific active states:hoverActive
,touchActive
, andkeyActive
, and is not a state in its own right. - The
focus
prop is just a convenience wrapper around the 3focusFrom
states:tab
,mouse
andtouch
, and is not a state in its own right.
More Examples
Using Interactive's State in Parent Component
import React from 'react';
import Interactive from 'react-interactive';
class MyComponent extends React.Component {
constructor() {
super();
this.state = {
iState: 'normal',
focus: false,
};
}
handleOnStateChange = ({ nextState }) => {
this.setState(nextState);
// equivalent to the line above:
// this.setState({
// iState: nextState.iState,
// focus: nextState.focus,
// });
}
render() {
return (
<div>
<Interactive
as="div"
onStateChange={this.handleOnStateChange}
// ...and any other props as needed
>RI component</Interactive>
{
// create your component using:
// this.state.iState === 'normal' / 'hover' / 'hoverActive' / 'touchActive' / 'keyActive'
// this.state.focus === false / 'tab' / 'mouse' / 'touch'
}
</div>
);
}
}
Enter or Leave a Specific State Hook
- Note that this example is written as a ReactFunctionalComponent, but the same
handleOnStateChange
logic would apply when creating a ReactClass.
import React from 'react';
import Interactive from 'react-interactive';
function MyFunctionalComponent() {
function enterFocus() {
// do something when enter the focus state
}
function leaveFocus() {
// do something when leave the focus state
}
function enterTouchActive() {
// do something when enter the touchActive state
}
function leaveTouchActive() {
// do something when leave the touchActive state
}
function handleOnStateChange({ prevState, nextState }) {
!prevState.focus && nextState.focus && enterFocus();
prevState.focus && !nextState.focus && leaveFocus();
if (nextState.iState === 'touchActive' && prevState.iState !== nextState.iState) {
enterTouchActive();
} else if (prevState.iState === 'touchActive' && prevState.iState !== nextState.iState) {
leaveTouchActive();
}
}
return (
<Interactive
as="div"
onStateChange={handleOnStateChange}
// ...and any other props as needed
>RI component</Interactive>
);
}
hover
and active
Show On - Show Div1 if the mouse is on the React Interactive element, that is, RI is in the
hover
orhoverActive
state. - Show Div2 if RI is in an
active
state, one ofhoverActive
,touchActive
, orkeyActive
. - Note that both Div1 and Div2 will be shown when RI is in the
hoverActive
state.
import React from 'react';
import Interactive from 'react-interactive';
class MyComponent extends React.Component {
constructor() {
super();
this.state = {
hover: false,
active: false,
};
}
handleOnStateChange = ({ nextState }) => {
this.setState({
// hover and hoverActive both contain hover, so check nextState for hover
hover: /hover/.test(nextState.iState),
// hoverActive, touchActive, and keyActive all contain Active, note the capitalization
active: /Active/.test(nextState.iState),
});
}
render() {
return (
<div>
<Interactive
as="div"
onStateChange={this.handleOnStateChange}
// ...and any other props as needed
>RI element</Interactive>
{this.state.hover && <div>Div1 shown if RI is in the hover or hoverActive state</div>}
{this.state.active && <div>Div2 shown if RI is in one of the active states</div>}
</div>
);
}
}
hover
, touchActive
and focusFromTab
Show On import React from 'react';
import Interactive from 'react-interactive';
class MyComponent extends React.Component {
constructor() {
super();
this.state = {
showInfo: false,
};
}
shouldComponentUpdate(nextProps, nextState) {
return this.state.showInfo !== nextState.showInfo;
}
handleOnStateChange = ({ nextState }) => {
this.setState({
showInfo:
nextState.iState === 'hover' ||
nextState.iState === 'touchActive' ||
nextState.focus === 'tab'
});
}
render() {
return (
<div>
{this.state.showInfo && <div>Some info about something</div>}
<Interactive
as="div"
onStateChange={this.handleOnStateChange}
// ...and any other props as needed
>Show info</Interactive>
</div>
);
}
}
as
Hot Swappable - Hot-swap JSX/ReactElements while loading something.
- Seamlessly maintains the current interactive state while allowing for separate interactive styling of the two JSX elements.
- Note that the
onClick
prop is only present on theclickToLoad
JSX element and not on thecurrentlyLoading
element, so any clicks that come through while loading will be ignored.
import React, { PropTypes } from 'react';
import Interactive from 'react-interactive';
class MyComponent extends React.Component {
static propTypes = {
load: PropTypes.func.isRequired,
}
constructor() {
super();
this.state = {
loading: false,
};
}
loadSomething = () => {
this.setState({ loading: true });
this.props.load(() => {
this.setState({ loading: false });
});
}
render() {
const clickToLoad = (
<span
onClick={this.loadSomething}
hover={{ color: 'green' }}
active={{ color: 'blue' }}
focusFromTab={{ outline: '2px solid green' }}
>Load Something</span>
);
const currentlyLoading = (
<span
hover={{ color: 'gray' }}
active={{ color: 'lightgray' }}
focusFromTab={{ outline: '2px solid gray' }}
>Loading...</span>
);
return (
<Interactive
as={this.state.loading ? currentlyLoading : clickToLoad}
style={{ fontSize: '14px', padding: '5px' }}
normal={{ color: 'black' }}
/>
);
}
}