Skip to content

Commit 5d94289

Browse files
committed
feat(focusable): add focusable component, cypress-tab-plugin
add cypress tests add prop for dynamically controlling focusability/classname
1 parent 68df602 commit 5d94289

File tree

14 files changed

+632
-2
lines changed

14 files changed

+632
-2
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import * as React from 'react';
2+
import { css } from '@patternfly/react-styles';
3+
4+
export interface FocusableProps {
5+
children?: string | React.ReactElement;
6+
/** any additional classes to apply to the focusable element */
7+
className?: string;
8+
/** An HTML element to use as the container */
9+
component?: React.ElementType;
10+
/** Sets the tabindex value */
11+
tabIndex?: number;
12+
/** Whether an intermediary DOM node container is needed */
13+
withContainer?: boolean;
14+
/** A prop for dynamically controlling focusability */
15+
isFocusable?: boolean;
16+
}
17+
18+
const renderElement = ({ children, tabIndex, className, component: Component, ...props }: FocusableProps) =>
19+
typeof children === 'object'
20+
? React.cloneElement(children as React.ReactElement, {
21+
tabIndex,
22+
className: css(className),
23+
...props
24+
})
25+
: React.createElement(Component, { tabIndex, className: css(className), ...props }, children);
26+
27+
const Focusable: React.FunctionComponent<FocusableProps> = ({
28+
children,
29+
className,
30+
component: Component = 'div',
31+
tabIndex = 0,
32+
isFocusable = true,
33+
withContainer = false,
34+
...props
35+
}) => {
36+
const focusableElement = children as React.ReactElement;
37+
38+
const getDefaultTabIndex = () =>
39+
!isFocusable || focusableElement.props.isDisabled || focusableElement.props.disabled ? tabIndex : null;
40+
41+
return withContainer ? (
42+
<Component className={css(className)} tabIndex={getDefaultTabIndex()} {...props}>
43+
{children}
44+
</Component>
45+
) : (
46+
renderElement({ children, tabIndex, className, component: Component, ...props })
47+
);
48+
};
49+
50+
export { Focusable };
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import * as React from 'react';
2+
import { shallow } from 'enzyme';
3+
import { Card } from '../../Card/Card';
4+
import { Button } from '../../Button/Button';
5+
import { Focusable } from '../Focusable';
6+
7+
test('Focusable', () => {
8+
const view = shallow(<Focusable>test</Focusable>);
9+
expect(view).toMatchSnapshot();
10+
});
11+
12+
test('Focus non-interactive html', () => {
13+
const view = shallow(
14+
<Focusable aria-label="Example focusable article">
15+
<article>Article element</article>
16+
</Focusable>
17+
);
18+
expect(view).toMatchSnapshot();
19+
});
20+
21+
test('Focus non-interactive component', () => {
22+
const view = shallow(
23+
<Focusable aria-label="Example focusable Card with positive tabIndex" tabIndex={2}>
24+
<Card>Card element</Card>
25+
</Focusable>
26+
);
27+
expect(view).toMatchSnapshot();
28+
});
29+
30+
test('Focus disabled button', () => {
31+
const view = shallow(
32+
<Focusable withContainer component="span">
33+
<Button isDisabled>Button text</Button>
34+
</Focusable>
35+
);
36+
expect(view).toMatchSnapshot();
37+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`Focus disabled button 1`] = `
4+
<span
5+
className=""
6+
tabIndex={0}
7+
>
8+
<Component
9+
isDisabled={true}
10+
>
11+
Button text
12+
</Component>
13+
</span>
14+
`;
15+
16+
exports[`Focus non-interactive component 1`] = `
17+
<Card
18+
aria-label="Example focusable Card with positive tabIndex"
19+
className=""
20+
tabIndex={2}
21+
>
22+
Card element
23+
</Card>
24+
`;
25+
26+
exports[`Focus non-interactive html 1`] = `
27+
<article
28+
aria-label="Example focusable article"
29+
className=""
30+
tabIndex={0}
31+
>
32+
Article element
33+
</article>
34+
`;
35+
36+
exports[`Focusable 1`] = `
37+
<div
38+
className=""
39+
tabIndex={0}
40+
>
41+
test
42+
</div>
43+
`;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
---
2+
title: 'Focusable'
3+
section: components
4+
cssPrefix: 'pf-c-focusable'
5+
typescript: true
6+
propComponents: ['Focusable']
7+
beta: true
8+
---
9+
10+
import { Focusable, Button, Tooltip, Radio, Card, CardHeader, CardBody, CardFooter } from '@patternfly/react-core';
11+
import { BeerIcon } from '@patternfly/react-icons';
12+
13+
## Examples
14+
```js title=Focus-text
15+
import React from 'react';
16+
import { Focusable } from '@patternfly/react-core';
17+
18+
class FocusableText extends React.Component {
19+
constructor(props) {
20+
super(props);
21+
}
22+
render() {
23+
return (
24+
<Focusable>
25+
This text is focusable
26+
</Focusable>
27+
);
28+
}
29+
}
30+
```
31+
32+
```js title=Focus-non-interactive-html-children
33+
import React from 'react';
34+
import { Focusable, Button } from '@patternfly/react-core';
35+
36+
class FocusNonInteractiveHtmlChildren extends React.Component {
37+
constructor(props) {
38+
super(props);
39+
}
40+
render() {
41+
return (
42+
<Focusable aria-label="Example focusable article">
43+
<article>Article element</article>
44+
</Focusable>
45+
);
46+
}
47+
}
48+
```
49+
50+
```js title=Focus-non-interactive-component-children
51+
import React from 'react';
52+
import { Focusable, Button, Card, CardHeader, CardBody, CardFooter } from '@patternfly/react-core';
53+
54+
class FocusNonInteractiveComponentChildren extends React.Component {
55+
constructor(props) {
56+
super(props);
57+
}
58+
render() {
59+
return (
60+
<Focusable>
61+
<Card>
62+
<CardHeader>Header</CardHeader>
63+
<CardBody>Body</CardBody>
64+
<CardFooter>Footer</CardFooter>
65+
</Card>
66+
</Focusable>
67+
);
68+
}
69+
}
70+
```
71+
72+
```js title=Focus-with-positive-tabindex
73+
import React from 'react';
74+
import { Focusable } from '@patternfly/react-core';
75+
76+
class FocusPositiveTabindex extends React.Component {
77+
constructor(props) {
78+
super(props);
79+
}
80+
render() {
81+
return (
82+
<Focusable tabIndex={1}>
83+
<button className="pf-c-button pf-m-tertiary">First focusable element on the page (example)</button>
84+
</Focusable>
85+
);
86+
}
87+
}
88+
```
89+
90+
```js title=Focus-an-icon
91+
import React from 'react';
92+
import { Focusable, Button, Tooltip } from '@patternfly/react-core';
93+
import { BeerIcon } from '@patternfly/react-icons';
94+
95+
class FocusIcon extends React.Component {
96+
constructor(props) {
97+
super(props);
98+
}
99+
render() {
100+
return (
101+
<Tooltip content="Focus on beer">
102+
<Focusable>
103+
<BeerIcon />
104+
</Focusable>
105+
</Tooltip>
106+
);
107+
}
108+
}
109+
```
110+
111+
```js title=Wrapping-disabled-button-with-tooltip
112+
import React from 'react';
113+
import { Focusable, Button, Tooltip } from '@patternfly/react-core';
114+
115+
class WrapDisabledButtonTooltip extends React.Component {
116+
constructor(props) {
117+
super(props);
118+
}
119+
120+
render() {
121+
return (
122+
<Tooltip content="Disabled button tooltip content">
123+
<Focusable withContainer component="span">
124+
<Button isDisabled onClick={() => {console.log('click event fired for disabled button')}}>Disabled button text</Button>
125+
</Focusable>
126+
</Tooltip>
127+
);
128+
}
129+
}
130+
```
131+
132+
```js title=Wrapping-disabled-html-button-with-tooltip
133+
import React from 'react';
134+
import { Focusable, Button, Tooltip } from '@patternfly/react-core';
135+
136+
class WrapDisabledButtonTooltip extends React.Component {
137+
constructor(props) {
138+
super(props);
139+
}
140+
141+
render() {
142+
return (
143+
<Tooltip content="Disabled button tooltip content">
144+
<Focusable withContainer component="span">
145+
<button disabled onClick={() => {console.log('click event fired for html disabled button')}}>Disabled button text</button>
146+
</Focusable>
147+
</Tooltip>
148+
);
149+
}
150+
}
151+
```
152+
153+
```js title=Wrapping-disabled-radio-with-tooltip
154+
import React from 'react';
155+
import { Focusable, Radio, Tooltip } from '@patternfly/react-core';
156+
157+
class WrapDisabledRadioTooltip extends React.Component {
158+
constructor(props) {
159+
super(props);
160+
}
161+
render() {
162+
return (
163+
<Tooltip content="Disabled radio tooltip content">
164+
<Focusable withContainer>
165+
<Radio isDisabled label="Disabled radio with tooltip" id="disabled-radio-with-tooltip" name="radio-1" />
166+
</Focusable>
167+
</Tooltip>
168+
);
169+
}
170+
}
171+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './Focusable';

packages/react-core/src/components/Tooltip/examples/Tooltip.md

+13-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ typescript: true
66
propComponents: ['Tooltip']
77
---
88

9-
import { Button, Tooltip, TooltipPosition, Checkbox } from '@patternfly/react-core';
9+
import { Button, Tooltip, TooltipPosition, Checkbox, Focusable } from '@patternfly/react-core';
1010
import { OutlinedQuestionCircleIcon } from '@patternfly/react-icons';
1111

1212
## Examples
@@ -54,6 +54,18 @@ import { OutlinedQuestionCircleIcon } from '@patternfly/react-icons';
5454
</div>
5555
```
5656

57+
```js title=On-disabled-element
58+
import React from 'react';
59+
import { Tooltip, TooltipPosition, Button, Focusable } from '@patternfly/react-core';
60+
import { OutlinedQuestionCircleIcon } from '@patternfly/react-icons';
61+
62+
<Tooltip content="Tooltip content">
63+
<Focusable withContainer component="span">
64+
<Button isDisabled>Button text</Button>
65+
</Focusable>
66+
</Tooltip>
67+
```
68+
5769
```js title=Positions
5870
import React from 'react';
5971
import { Button, Tooltip, TooltipPosition, Checkbox } from '@patternfly/react-core';

packages/react-core/src/components/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export * from './DataList';
2020
export * from './Dropdown';
2121
export * from './EmptyState';
2222
export * from './Expandable';
23+
export * from './Focusable';
2324
export * from './Form';
2425
export * from './FormSelect';
2526
export * from './InputGroup';

0 commit comments

Comments
 (0)