Skip to content

Commit eba09ca

Browse files
committed
feat(focusable): add focusable component, cypress-tab-plugin
add cypress tests add prop for dynamically controlling focusability/classname
1 parent 3167f05 commit eba09ca

File tree

15 files changed

+614
-2
lines changed

15 files changed

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

packages/patternfly-4/react-core/src/components/Tabs/Tab.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,5 @@ export interface TabProps extends Omit<React.HTMLProps<HTMLAnchorElement | HTMLB
1818
tabContentRef?: React.RefObject<any>;
1919
}
2020

21+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
2122
export const Tab: React.FunctionComponent<TabProps> = ({ className = '' }: TabProps) => null;

packages/patternfly-4/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/patternfly-4/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)