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(react): add unstable_IconButton #10095

Merged
1 change: 1 addition & 0 deletions packages/carbon-react/__tests__/index-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ Array [
"HeaderNavigation",
"HeaderPanel",
"HeaderSideNavItems",
"IconButton",
"IconSkeleton",
"InlineLoading",
"InlineNotification",
Expand Down
1 change: 1 addition & 0 deletions packages/carbon-react/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ export {
unstable_Tooltip as Tooltip,
unstable_Popover as Popover,
unstable_PopoverContent as PopoverContent,
unstable_IconButton as IconButton,
} from 'carbon-components-react';

export {
Expand Down
48 changes: 48 additions & 0 deletions packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -9183,5 +9183,53 @@ Map {
},
},
},
"unstable_IconButton" => Object {
"$$typeof": Symbol(react.forward_ref),
"propTypes": Object {
"align": Object {
"args": Array [
Array [
"top",
"top-left",
"top-right",
"bottom",
"bottom-left",
"bottom-right",
"left",
"right",
],
],
"type": "oneOf",
},
"children": Object {
"type": "node",
},
"defaultOpen": Object {
"type": "bool",
},
"enterDelayMs": Object {
"type": "number",
},
"kind": Object {
"args": Array [
Array [
"primary",
"secondary",
"ghost",
"tertiary",
],
],
"type": "oneOf",
},
"label": Object {
"isRequired": true,
"type": "node",
},
"leaveDelayMs": Object {
"type": "number",
},
},
"render": [Function],
},
}
`;
1 change: 1 addition & 0 deletions packages/react/src/__tests__/index-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ Array [
"unstable_FeatureFlags",
"unstable_HStack",
"unstable_Heading",
"unstable_IconButton",
"unstable_Menu",
"unstable_MenuDivider",
"unstable_MenuGroup",
Expand Down
33 changes: 27 additions & 6 deletions packages/react/src/components/Button/Button.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,16 +141,17 @@ const Button = React.forwardRef(function Button(
[`${prefix}--btn--${kind}`]: kind,
[`${prefix}--btn--disabled`]: disabled,
[`${prefix}--btn--expressive`]: isExpressive,
[`${prefix}--tooltip--visible`]: isHovered,
[`${prefix}--tooltip--hidden`]: hasIconOnly && !allowTooltipVisibility,
[`${prefix}--tooltip--visible`]: !enabled && isHovered,
[`${prefix}--tooltip--hidden`]:
!enabled && hasIconOnly && !allowTooltipVisibility,
[`${prefix}--btn--icon-only`]: hasIconOnly,
[`${prefix}--btn--selected`]: hasIconOnly && isSelected && kind === 'ghost',
[`${prefix}--tooltip__trigger`]: hasIconOnly,
[`${prefix}--tooltip--a11y`]: hasIconOnly,
[`${prefix}--tooltip__trigger`]: !enabled && hasIconOnly,
[`${prefix}--tooltip--a11y`]: !enabled && hasIconOnly,
[`${prefix}--btn--icon-only--${tooltipPosition}`]:
hasIconOnly && tooltipPosition,
!enabled && hasIconOnly && tooltipPosition,
[`${prefix}--tooltip--align-${tooltipAlignment}`]:
hasIconOnly && tooltipAlignment,
!enabled && hasIconOnly && tooltipAlignment,
});

const commonProps = {
Expand Down Expand Up @@ -214,6 +215,26 @@ const Button = React.forwardRef(function Button(
otherProps = anchorProps;
}

if (enabled) {
delete otherProps['aria-describedby'];

return React.createElement(
component,
{
onMouseEnter,
onMouseLeave,
onFocus,
onBlur,
onClick,
type,
...other,
...commonProps,
...otherProps,
},
children
);
}

return React.createElement(
component,
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* Copyright IBM Corp. 2016, 2018
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/

import { Edit16 } from '@carbon/icons-react';
import { render, screen } from '@testing-library/react';
import React from 'react';
import { IconButton } from '../';
import { FeatureFlags } from '../../FeatureFlags';

describe('IconButton', () => {
it('should support labelling with label', () => {
render(
<FeatureFlags flags={{ 'enable-v11-release': true }}>
<IconButton label="edit">
<Edit16 />
</IconButton>
</FeatureFlags>
);
expect(screen.getByLabelText('edit')).toBeInTheDocument();
});

it('should support data-testid on the <button> element', () => {
render(
<FeatureFlags flags={{ 'enable-v11-release': true }}>
<IconButton label="edit" data-testid="icon-button">
<Edit16 />
</IconButton>
</FeatureFlags>
);
const button = screen.getByTestId('icon-button');
expect(button).toBeInTheDocument();
expect(button.tagName).toBe('BUTTON');
});

it('should forward extra props to the underlying <button> element', () => {
render(
<FeatureFlags flags={{ 'enable-v11-release': true }}>
<IconButton label="edit" data-testid="icon-button" disabled>
<Edit16 />
</IconButton>
</FeatureFlags>
);
expect(screen.getByTestId('icon-button')).toHaveAttribute('disabled');
});

it('should support a `ref` on the underlying <button> element', () => {
const ref = jest.fn();
render(
<FeatureFlags flags={{ 'enable-v11-release': true }}>
<IconButton label="edit" data-testid="icon-button" ref={ref}>
<Edit16 />
</IconButton>
</FeatureFlags>
);
expect(ref).toHaveBeenCalledWith(screen.getByTestId('icon-button'));
});
});
91 changes: 91 additions & 0 deletions packages/react/src/components/IconButton/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/**
* Copyright IBM Corp. 2016, 2018
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/

import PropTypes from 'prop-types';
import React from 'react';
import Button from '../Button';
import { Tooltip } from '../Tooltip/next';
import { usePrefix } from '../../internal/usePrefix';

const IconButton = React.forwardRef(function IconButton(props, ref) {
const {
align,
children,
defaultOpen = false,
enterDelayMs,
kind,
label,
leaveDelayMs,
...rest
} = props;
const prefix = usePrefix();

return (
<Tooltip
align={align}
className={`${prefix}--icon-tooltip`}
defaultOpen={defaultOpen}
enterDelayMs={enterDelayMs}
label={label}
leaveDelayMs={leaveDelayMs}>
<Button {...rest} hasIconOnly kind={kind} ref={ref} size="sm">
{children}
</Button>
</Tooltip>
);
});

IconButton.propTypes = {
/**
* Specify how the trigger should align with the tooltip
*/
align: PropTypes.oneOf([
'top',
'top-left',
'top-right',
'bottom',
'bottom-left',
'bottom-right',
'left',
'right',
]),

/**
* Provide an icon or asset to be rendered inside of the IconButton
*/
children: PropTypes.node,

/**
* Specify whether the tooltip should be open when it first renders
*/
defaultOpen: PropTypes.bool,

/**
* Specify the duration in milliseconds to delay before displaying the tooltip
*/
enterDelayMs: PropTypes.number,

/**
* Specify the type of button to be used as the base for the IconButton
*/
kind: PropTypes.oneOf(['primary', 'secondary', 'ghost', 'tertiary']),

/**
* Provide the label to be rendered inside of the Tooltip. The label will use
* `aria-labelledby` and will fully describe the child node that is provided.
* This means that if you have text in the child node it will not be
* announced to the screen reader.
*/
label: PropTypes.node.isRequired,

/**
* Specify the duration in milliseconds to delay before hiding the tooltip
*/
leaveDelayMs: PropTypes.number,
};

export { IconButton };
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/**
* Copyright IBM Corp. 2016, 2018
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/

import { Edit16 } from '@carbon/icons-react';
import React from 'react';
import { IconButton } from '../';

export default {
title: 'Experimental/unstable_IconButton',
component: IconButton,
parameters: {
controls: {
hideNoControlsWarning: true,
},
layout: 'centered',
},
argTypes: {
children: {
table: {
disable: true,
},
},
className: {
table: {
disable: true,
},
},
},
};

const PlaygroundStory = (props) => {
const { align, defaultOpen, disabled, kind, label } = props;
return (
<IconButton
align={align}
defaultOpen={defaultOpen}
disabled={disabled}
kind={kind}
label={label}>
<Edit16 />
</IconButton>
);
};

export const Playground = PlaygroundStory.bind({});

Playground.argTypes = {
align: {
defaultValue: 'bottom',
options: [
'top',
'top-left',
'top-right',
'bottom',
'bottom-left',
'bottom-right',
'left',
'right',
],
control: {
type: 'select',
},
},
defaultOpen: {
defaultValue: true,
},
disabled: {
defaultValue: false,
control: {
type: 'boolean',
},
},
label: {
control: {
type: 'text',
},
defaultValue: 'Custom label',
},
kind: {
control: {
type: 'select',
},
defaultValue: 'primary',
options: ['primary', 'secondary', 'ghost', 'tertiary'],
},
};
3 changes: 2 additions & 1 deletion packages/react/src/components/Tooltip/next/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ function Tooltip({
const triggerProps = {
onFocus: () => setOpen(true),
onBlur: () => setOpen(false),
// This should be placed on the trigger in case the element is disabled
onMouseEnter,
};

if (label) {
Expand Down Expand Up @@ -73,7 +75,6 @@ function Tooltip({
dropShadow={false}
highContrast
onKeyDown={onKeyDown}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
open={open}
ref={containerRef}>
Expand Down
1 change: 1 addition & 0 deletions packages/react/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -244,3 +244,4 @@ export {
export { Tooltip as unstable_Tooltip } from './components/Tooltip/next';
export { ContainedTab as unstable_ContainedTab } from './components/Tab';
export { ContainedTabs as unstable_ContainedTabs } from './components/Tabs';
export { IconButton as unstable_IconButton } from './components/IconButton';
Loading