Skip to content

Instantly share code, notes, and snippets.

@diegohaz
Last active September 11, 2019 21:15
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save diegohaz/f0b3b198745e381c37c60f317212d546 to your computer and use it in GitHub Desktop.
Save diegohaz/f0b3b198745e381c37c60f317212d546 to your computer and use it in GitHub Desktop.
Reakit Hooks API
import React from "react";
import { useButton, mergeProps } from "reakit";
function Button(props) {
// useButton should be used when the props are being applied to a non-button element
const button = useButton();
return <div {...mergeProps(button, props)} />;
}
// return value of useButton
const button = {
ref: Ref, // it'll be used to determine whether the element is an actual button tag
// if it's not a button tag, return those props
role: "button",
tabIndex: 0,
onKeyPress: e => {
// enter or space
if (e.charCode === 32 || e.charCode === 13) {
e.preventDefault();
e.target.click();
}
}
};
import React from "react";
import { useTooltipState, Tooltip, TooltipArrow, TooltipReference } from "reakit";
function App() {
const state = useTooltipState({ placement: "top" });
return (
<div>
<TooltipReference {...state}>Hover me</TooltipReference>
<Tooltip {...state}>
<TooltipArrow {...state} />
Tooltip
</Tooltip>
</div>
);
}
import { useAlert, mergeProps } from "reakit";
function Confirm({ onConfirm, ...props }) {
// if backdrop is being used, alert.role is set to "alertdialog", instead of "alert"
// https://www.w3.org/TR/wai-aria-1.1/#alertdialog
const { alert, backdrop, closeButton, autoFocus, state } = useAlert();
const handleConfirm = () => {
state.close();
onConfirm();
};
return state.mounted ? (
<React.Fragment>
<div {...backdrop} />
<div {...mergeProps(alert, props)}>
{props.children}
<button {...mergeProps(autoFocus, { onClick: state.close })}>Cancel</button>
<button onClick={handleConfirm}>Confirm</button>
</div>
</React.Fragment>
) : null;
}
// return value of useAlert
const alert = {
alert: {
id: "alertid", // some auto generated id
role: "alert", // or "alertdialog" if backdrop.ref is truthy
hidden: true,
style: {
position: "fixed", // if it's alertdialog
display: "none"
}
},
backdrop: {
ref: Ref, // if it's truthy, disables focus and scroll on the elements behind
role: "presentation",
hidden: true,
onClick: () => state.close()
},
button: {
// uses useButton (which inclused ref, role, tabIndex etc.)
"aria-haspopup": "dialog", // if it's alertdialog
"aria-controls": "alertid",
"aria-expanded": "false",
onClick: () => state.toggle()
},
autoFocus: {
ref: Ref, // uses it to .focus() when state.visible becomes true
tabIndex: 0
},
state: {
mounted: true,
transitioning: false,
visible: true,
open: () => {},
close: () => {},
toggle: () => {}
}
};
import { useDropdown, useDropdownOption, mergeProps } from "reakit";
// https://www.w3.org/TR/wai-aria-1.1/#listbox
// https://www.w3.org/TR/wai-aria-practices/#Listbox
function Dropdown(props) {
// list is portal
const { dropdown, button, list, state } = useDropdown();
const option1 = useDropdownOption({ id: "option1", ...state });
const option2 = useDropdownOption({ id: "option2", ...state });
return (
<div {...mergeProps(dropdown, props)}>
<button {...button} />
{state.mounted && (
<ul {...list}>
<li {...option1} />
<li {...option2} />
</ul>
)}
</div>
);
}
// return value of useDropdown
const dropdown = {
dropdown: {
// if input.ref is truthy (don't know what to do when it's not combobox)
// https://www.w3.org/TR/wai-aria-1.1/#combobox
role: "combobox",
"aria-expanded": "true",
"aria-haspopup": "listbox" // or grid
},
button: {
// uses useButton (which inclused ref, role, tabIndex etc.)
role: "button",
onClick: () => state.toggle(),
"aria-controls": "listid" // or gridid
},
input: {
role: "textbox",
onFocus: () => state.open(),
onBlur: () => state.close(),
onKeyPress: () => {}, // key arrows to select options
"aria-controls": "listid", // or gridid
"aria-autocomplete": "foo" // if autoComplete = true
},
list: {
// return value of useDropdownList
role: "listbox",
id: "listid", // some auto generated id
onKeyPress: () => {} // key arrows if button.ref is truthy (not input, so focus goes here)
},
grid: {
// return value of useGrid
role: "grid",
id: "gridid",
onKeyPress: () => {} // key arrows if button.ref is truthy (not input, so focus goes here)
},
state: {
mounted: true,
transitioning: false,
visible: true,
open: () => {},
close: () => {},
toggle: () => {}
}
};
import { useDropdown, useDropdownOption, mergeProps } from "reakit";
function DropdownEditable(props) {
const { dropdown, input, list, state } = useDropdown({ autoComplete: true });
const option1 = useDropdownOption({ id: "option1", ...state });
const option2 = useDropdownOption({ id: "option2", ...state });
return (
<div {...mergeProps(dropdown, props)}>
<input {...input} />
{state.mounted && (
<ul {...list}>
<li {...option1} />
<li {...option2} />
</ul>
)}
</div>
);
}
import { useDropdown, useDropdownOption, mergeProps } from "reakit";
function DropdownMulti(props) {
const { dropdown, input, list, state } = useDropdown({ multi: true });
const option1 = useDropdownOption({ id: "option1", ...state });
const option2 = useDropdownOption({ id: "option2", ...state });
return (
<div {...mergeProps(dropdown, props)}>
<ul>
{state.selected.map(option => (
<li key={option}>
{option}
<button onClick={() => state.remove(option)}>x</button>
</li>
))}
</ul>
<input {...input} />
{state.mounted && (
<ul {...list}>
<li {...option1} />
<li {...option2} />
</ul>
)}
</div>
);
}
import { useGrid, useGridRow, useGridCell, mergeProps } from "reakit";
function Cell({ state, ...props }) {
const cell = useGridCell(state);
return <div {...mergeProps(cell, props)} />;
}
function Row({ state, children, ...props }) {
const { row, state: rowState } = useGridRow(state);
return (
<div {...mergeProps(row, props)}>
{children(state)}
</div>
);
}
// https://www.w3.org/TR/wai-aria-1.1/#grid
function Grid(props) {
const { grid, state } = useGrid();
const data = {
row0: ["cell0", "cell1", "cell2"],
row1: ["cell0", "cell1", "cell2"],
row2: ["cell0", "cell1", "cell2"],
};
return (
<div {...mergeProps(grid, props)}>
{Object.entries(data).map(([row, cells]) => (
<Row key={row} state={state}>
{rowState => cells.map(cell => (
<Cell key={cell} state={rowState}>{cell}</Cell>
))}
</Row>
))}
</div>
);
}
// return value of useGrid
const grid = {
grid: {
role: "grid",
onKeyPress: () => {}
},
state: {
rows: [
[ref, ref, ref, ref, ref],
[ref, ref, ref, ref, ref]
],
addRow: () => {}
}
};
import React from "react";
import { useHidden } from "reakit";
function Hidden() {
// hidden contains props like `style.display`, `aria-hidden`
// button contains props like `onClick`
// state has `visible`, `mounted` etc.
// I'm still not sure if transitions should be handled here or in another hook
const { hidden, button, state } = useHidden({ visible: true });
return (
<div>
<button {...button}>Toggle</button>
{state.mounted && <div {...hidden}>Hidden</div>}
</div>
);
}
// return value of useHidden
const hidden = {
hidden: {
hidden: true,
"aria-hidden": "true"
},
button: {
onClick: () => state.toggle()
},
state: {
mounted: false,
transitioning: false,
visible: true,
show: () => {},
hide: () => {},
toggle: () => {}
}
};
import { useSkipNav } from "reakit";
function Layout({ children }) {
const { skipNav, content } = useSkipNav();
return (
<React.Fragment>
<div {...skipNav} />
<Nav />
<div {...content}>{children}</div>
</React.Fragment>
);
}
// return value of useSkipNav
const skipNav = {
skipNav: {
tabIndex: 0,
onClick: () => {}
},
content: {
id: "contentid" // useId (takes from a global map of refs)
}
};
import React from "react";
import { useDialog, mergeProps } from "reakit";
function LoginModalButton(props) {
// all state hooks implement useContextState from Constate underneath
// this means that we can pass a "context" prop so as to access the shared state
const { button } = useDialog({ context: "login" });
return <button {...mergeProps(button, props)} />;
}
function LoginModal(props) {
const { dialog, backdrop, autoFocus, state } = useDialog({ context: "login" });
return state.mounted ? (
<React.Fragment>
<div {...backdrop} />
<div {...mergeProps(dialog, props)}>
<button {...mergeProps(autoFocus, { onClick: state.close })} />
</div>
</React.Fragment>
) : null;
}
// return value of useDialog
const dialog = {
dialog: {},
backdrop: {},
autoFocus: {},
state: {}
};
import { useMenu, useMenuItem } from "reakit";
function Menu() {
// it has orientation="vertical" by default
const { menu, separator, button, state } = useMenu({ orientation: "vertical" });
const item1 = useMenuItem(state);
const submenu = useMenu();
const subitem1 = useMenuItem(submenu.state);
// submenu.button sets aria-haspopup="menu"
return (
<React.Fragment>
<button {...button}>Menu</button>
{state.mounted && (
<ul {...menu}>
<li {...mergeProps(item1, submenu.button)}>
<ul {...submenu.menu}>
<li {...subitem1} />
</ul>
</li>
</ul>
)}
</React.Fragment>
);
}
// return value of useMenu
const menu = {
menu: {},
separator: {},
button: {},
state: {}
};
import React from "react";
import { usePopover, mergeProps } from "reakit";
function Popover() {
// all main hooks (e.g. useModule) return a map of objects
// Popover is portal by default
// Popover is dialog by default
const { popover, arrow, button, state } = usePopover({ placement: "top" });
return (
<div>
<button {...button}>Toggle</button>
{state.mounted && (
<div {...popover}>
<div {...arrow} />
Popover
</div>
)}
</div>
);
}
// return value of usePopover
const popover = {
popover: {},
arrow: {},
button: {},
state: {}
};
import React from "react";
import {
usePopoverState,
usePopoverArrow,
usePopoverButton,
usePopoverPopover
} from "reakit";
function PopoverDecoupled() {
// hooks are decoupled so users can plug their own
// child hooks have a naming convention based on keys returned by the main hooks
// e.g. { state, arrow, button, popover } = usePopover();
const state = usePopoverState({ placement: "top" });
const arrow = usePopoverArrow(state);
const button = usePopoverButton(state);
const popover = usePopoverPopover(state);
return (
<div>
<button {...button}>Toggle</button>
<div {...popover}>
<div {...arrow} />
Popover
</div>
</div>
);
}
import { usePortal, mergeProps } from "reakit";
// Popover, Tooltip, Dialog, Sidebar etc. all use portal by default
function Portal(props) {
const portal = usePortal();
return <div {...mergeProps(portal, props)} />
}
// return value of usePortal
const portal = {};
import { useProgress } from "reakit";
// https://www.w3.org/TR/wai-aria-1.1/#progressbar
function Progress() {
const { progress, reference } = useProgress();
return (
<React.Fragment>
<div {...reference} /> {/* sets aria-busy and aria-describedby */}
<div {...progress} />
</React.Fragment>
);
}
// return value of useProgress
const progress = {
progress: {
role: "progressbar",
id: "progressid"
},
reference: {
"aria-busy": "true",
"aria-describedby": "progressid"
},
state: {}
};
import React from "react";
import { Provider } from "reakit";
import emotionPlugin from "reakit-plugin-emotion";
const myOwnPlugin = {
usePopoverPopover: (input, output) => {
// It's OK to use inline style here.
// It'll be transformed later with emotionPlugin
const style = { color: "red" };
return mergeProps(output, { style });
}
}
const plugins = [myOwnPlugin, emotionPlugin];
function App() {
return (
<Provider plugins={plugins}>
...
</Provider>
);
}
import { mergeProps } from "reakit";
import { css } from "emotion";
// It would be called on every Reakit hook
// Since Reakit hooks will return inline styles, it transforms them
// into className using emotion
export function useOutput(input, { style, ...output }) {
const className = css(style);
return mergeProps(output, { className });
}
export default { useOutput };
import React from "react";
import { useSteps, useStep } from "reakit";
function Steps() {
const { steps, progress, previousButton, nextButton, state } = useSteps({ current: 0 });
const first = useStep(state);
const second = useStep(state);
const third = useStep(state);
return (
<div {...steps}>
<button {...previousButton}>Previous</button>
<button {...nextButton}>Next</button>
{first.state.mounted && <div {...first.step}>First</div>}
{second.state.mounted && <div {...second.step}>Second</div>}
{third.state.mounted && <div {...third.step}>Third</div>}
</div>
);
}
// return value of useSteps
const steps = {
steps: {},
progress: {},
previousButton: {},
nextButton: {},
state: {}
};
import React from "react";
import { useSteps, useStep } from "reakit";
function Step({ state, ...props }) {
const { step, state: stepState } = useStep(state);
return stepState.mounted && <div {...mergeProps(step, props)} />;
}
function StepsDynamic() {
const { steps, previousButton, nextButton, state } = useSteps({ current: 0 });
const items = ["First", "Second", "Third"];
return (
<div {...steps}>
<button {...previousButton}>Previous</button>
<button {...nextButton}>Next</button>
{items.map(step => (
<Step key={step} state={state}>{step}</Step>
))}
</div>
);
}
import React from "react";
import { useTabList, useTab } from "reakit";
function Tabs() {
// some modules have collections
// collections have their own state (e.g. useTabListState)
const { list, previousButton, nextButton, state } = useTabList({ current: 0 });
// first = { tab, panel, state }
// state can be useTabState, useTabPanelState or useHiddenState
const first = useTab(state);
const second = useTab(state);
const third = useTab(state);
return (
<div>
<ul {...list}>
<li><button {...previousButton}>Previous</button></li>
<li><button {...first.tab}>First</button></li>
<li><button {...second.tab}>Second</button></li>
<li><button {...third.tab}>Third</button></li>
<li><button {...nextButton}>Next</button></li>
</ul>
{first.state.mounted && <div {...first.panel}>First</div>}
{second.state.mounted && <div {...second.panel}>Second</div>}
{third.state.mounted && <div {...third.panel}>Third</div>}
</div>
);
}
import React from "react";
import {
useTabTab,
useTabPanel,
useTabListState,
useTabListList,
mergeProps
} from "reakit";
function Tab({ state, ...props }) {
// useTabTab implements useTheme("Tab") underneath
const tab = useTabTab(state);
return <button {...mergeProps(tab, props)} />;
}
function Panel({ state, ...props }) {
// panel has its own state, which can be useTabState, useTabPanelState or useHiddenState
// and that's why it's a map
const { panel, state: panelState } = useTabPanel(state);
return {panelState.mounted && <div {...mergeProps(panel, props)} />};
}
function TabsDynamic() {
// Decoupling state and tabs
// same as
// const { tabs, state } = useTabList({ current: 0 })
const state = useTabListState({ current: 0 });
const list = useTabListList(state);
const tabs = ["First", "Second", "Third"];
return (
<div>
<ul {...list}>
{tabs.map(tab => (
<li key={tab}><Tab state={state}>{tab}</Tab></li>
))}
</ul>
{tabs.map(tab => (
<Panel key={tab} state={state}>{tab}</Panel>
))}
</div>
);
}
import React from "react";
import { useToolbar, useToolbarItem, mergeProps } from "reakit";
function Toolbar() {
const { toolbar, state } = useToolbar({ orientation: "vertical" });
const item1 = useToolbarItem(state);
const item2 = useToolbarItem(state);
return (
<div {...toolbar}>
<button {...item1} />
<button {...item2} />
</div>
);
}
import React from "react";
import { useToolbar, useToolbarItem, mergeProps } from "reakit";
function ToolbarItem({ state, as: T = "div", ...props }) {
const item = useToolbarItem(state);
return <T {...mergeProps(item, props)} />;
}
function ToolbarDynamic() {
const { toolbar, state } = useToolbar();
const items = [{ as: MenuIcon }, { as: "img", src: "https://placekitten.com/150/200" }];
return (
<div {...toolbar}>
{items.map((props, i) => (
<ToolbarItem key={i} state={state} {...props} />
))}
</div>
);
}
import React from "react";
import { useTooltip } from "reakit";
function Tooltip() {
// useTooltip implements usePlugin("useTooltip") underneath
const { tooltip, arrow, reference, state } = useTooltip({ placement: "top" });
return (
<div>
<button {...reference}>Hover me</button>
{state.mounted && (
<div {...tooltip}>
<div {...arrow} />
Tooltip
</div>
)}
</div>
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment