Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(focusable): add focusable component #3845

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions packages/react-core/src/components/Focusable/Focusable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import * as React from 'react';
import { css } from '@patternfly/react-styles';

export interface FocusableProps {
children?: string | React.ReactElement;
/** Any additional classes to apply to the focusable element */
className?: string;
/** An HTML element to use as the container */
component?: React.ElementType;
/** Sets the tabindex value */
tabIndex?: number;
/** Whether an intermediary DOM node container is needed */
withContainer?: boolean;
/** A prop for dynamically controlling focusability */
isFocusable?: boolean;
}

const renderElement = ({ children, tabIndex, className, component: Component, ...props }: FocusableProps) =>
typeof children === 'object'
? React.cloneElement(children as React.ReactElement, {
tabIndex,
className: css(className),
...props
})
: React.createElement(Component, { tabIndex, className: css(className), ...props }, children);

const Focusable: React.FunctionComponent<FocusableProps> = ({
children,
className,
component: Component = 'div',
tabIndex = 0,
isFocusable = true,
withContainer = false,
...props
}) => {
const focusableElement = children as React.ReactElement;

const getDefaultTabIndex = () =>
!isFocusable || focusableElement.props.isDisabled || focusableElement.props.disabled ? tabIndex : null;

return withContainer ? (
<Component className={css(className)} tabIndex={getDefaultTabIndex()} {...props}>
{children}
</Component>
) : (
renderElement({ children, tabIndex, className, component: Component, ...props })
);
};

export { Focusable };
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import * as React from 'react';
import { shallow } from 'enzyme';
import { Card } from '../../Card/Card';
import { Button } from '../../Button/Button';
import { Focusable } from '../Focusable';

test('Focusable', () => {
const view = shallow(<Focusable>test</Focusable>);
expect(view).toMatchSnapshot();
});

test('Focus non-interactive html', () => {
const view = shallow(
<Focusable aria-label="Example focusable article">
<article>Article element</article>
</Focusable>
);
expect(view).toMatchSnapshot();
});

test('Focus non-interactive component', () => {
const view = shallow(
<Focusable aria-label="Example focusable Card with positive tabIndex" tabIndex={2}>
<Card>Card element</Card>
</Focusable>
);
expect(view).toMatchSnapshot();
});

test('Focus disabled button', () => {
const view = shallow(
<Focusable withContainer component="span">
<Button isDisabled>Button text</Button>
</Focusable>
);
expect(view).toMatchSnapshot();
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Focus disabled button 1`] = `
<span
className=""
tabIndex={0}
>
<Component
isDisabled={true}
>
Button text
</Component>
</span>
`;

exports[`Focus non-interactive component 1`] = `
<Card
aria-label="Example focusable Card with positive tabIndex"
className=""
tabIndex={2}
>
Card element
</Card>
`;

exports[`Focus non-interactive html 1`] = `
<article
aria-label="Example focusable article"
className=""
tabIndex={0}
>
Article element
</article>
`;

exports[`Focusable 1`] = `
<div
className=""
tabIndex={0}
>
test
</div>
`;
171 changes: 171 additions & 0 deletions packages/react-core/src/components/Focusable/examples/Focusable.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
---
title: 'Focusable'
section: components
cssPrefix: 'pf-c-focusable'
typescript: true
propComponents: ['Focusable']
beta: true
---

import { Focusable, Button, Tooltip, Radio, Card, CardHeader, CardBody, CardFooter } from '@patternfly/react-core';
import { BeerIcon } from '@patternfly/react-icons';

## Examples
```js title=Focus-text
import React from 'react';
import { Focusable } from '@patternfly/react-core';

class FocusableText extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<Focusable>
This text is focusable
</Focusable>
);
}
}
```

```js title=Focus-non-interactive-html-children
import React from 'react';
import { Focusable, Button } from '@patternfly/react-core';

class FocusNonInteractiveHtmlChildren extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<Focusable aria-label="Example focusable article">
<article>Article element</article>
</Focusable>
);
}
}
```

```js title=Focus-non-interactive-component-children
import React from 'react';
import { Focusable, Button, Card, CardHeader, CardBody, CardFooter } from '@patternfly/react-core';

class FocusNonInteractiveComponentChildren extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<Focusable>
<Card>
<CardHeader>Header</CardHeader>
<CardBody>Body</CardBody>
<CardFooter>Footer</CardFooter>
</Card>
</Focusable>
);
}
}
```

```js title=Focus-with-positive-tabindex
import React from 'react';
import { Focusable } from '@patternfly/react-core';

class FocusPositiveTabindex extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<Focusable tabIndex={1}>
<button className="pf-c-button pf-m-tertiary">First focusable element on the page (example)</button>
</Focusable>
);
}
}
```

```js title=Focus-an-icon
import React from 'react';
import { Focusable, Button, Tooltip } from '@patternfly/react-core';
import { BeerIcon } from '@patternfly/react-icons';

class FocusIcon extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<Tooltip content="Focus on beer">
<Focusable>
<BeerIcon />
</Focusable>
</Tooltip>
);
}
}
```

```js title=Wrapping-disabled-button-with-tooltip
import React from 'react';
import { Focusable, Button, Tooltip } from '@patternfly/react-core';

class WrapDisabledButtonTooltip extends React.Component {
constructor(props) {
super(props);
}

render() {
return (
<Tooltip content="Disabled button tooltip content">
<Focusable withContainer component="span">
<Button isDisabled onClick={() => {console.log('click event fired for disabled button')}}>Disabled button text</Button>
</Focusable>
</Tooltip>
);
}
}
```

```js title=Wrapping-disabled-html-button-with-tooltip
import React from 'react';
import { Focusable, Button, Tooltip } from '@patternfly/react-core';

class WrapDisabledButtonTooltip extends React.Component {
constructor(props) {
super(props);
}

render() {
return (
<Tooltip content="Disabled button tooltip content">
<Focusable withContainer component="span">
<button disabled onClick={() => {console.log('click event fired for html disabled button')}}>Disabled button text</button>
</Focusable>
</Tooltip>
);
}
}
```

```js title=Wrapping-disabled-radio-with-tooltip
import React from 'react';
import { Focusable, Radio, Tooltip } from '@patternfly/react-core';

class WrapDisabledRadioTooltip extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<Tooltip content="Disabled radio tooltip content">
<Focusable withContainer>
<Radio isDisabled label="Disabled radio with tooltip" id="disabled-radio-with-tooltip" name="radio-1" />
</Focusable>
</Tooltip>
);
}
}
```
1 change: 1 addition & 0 deletions packages/react-core/src/components/Focusable/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './Focusable';
14 changes: 13 additions & 1 deletion packages/react-core/src/components/Tooltip/examples/Tooltip.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ typescript: true
propComponents: ['Tooltip']
---

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

## Examples
Expand Down Expand Up @@ -54,6 +54,18 @@ import { OutlinedQuestionCircleIcon } from '@patternfly/react-icons';
</div>
```

```js title=On-disabled-element
import React from 'react';
import { Tooltip, TooltipPosition, Button, Focusable } from '@patternfly/react-core';
import { OutlinedQuestionCircleIcon } from '@patternfly/react-icons';

<Tooltip content="Tooltip content">
<Focusable withContainer component="span">
<Button isDisabled>Button text</Button>
</Focusable>
</Tooltip>
```

```js title=Positions
import React from 'react';
import { Button, Tooltip, TooltipPosition, Checkbox } from '@patternfly/react-core';
Expand Down
1 change: 1 addition & 0 deletions packages/react-core/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export * from './DataList';
export * from './Dropdown';
export * from './EmptyState';
export * from './Expandable';
export * from './Focusable';
export * from './Form';
export * from './FormSelect';
export * from './InputGroup';
Expand Down
Loading