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 toggletip component #10365

Merged
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* 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.
*/

describe('Toggletip', () => {
describe('accessibility', () => {
test.todo('accessibility-checker');
test.todo('axe');
});

// Usage
it.todo('should toggle visibility on click');
it.todo('should toggle visibility on Enter and Space');
it.todo('should close on Escape');
it.todo('should close if an element outside of the toggletip is clicked');

describe('Component API', () => {
it.todo('should support custom elements with the `as` prop');
it.todo('should support a custom class name with the `className` prop');
it.todo('should support different alignments with the `align` prop');
it.todo('should initially be open if `defaultOpen` is set to true');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* 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.
*/

describe('ToggletipActions', () => {
describe('Component API', () => {
it.todo('should support a custom class name with the `className` prop');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* 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.
*/

describe('ToggletipButton', () => {
describe('Component API', () => {
it.todo('should support a custom class name with the `className` prop');
it.todo('should use the `label` prop as the label for the <button>');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* 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.
*/

describe('ToggletipContent', () => {
describe('Component API', () => {
it.todo('should support a custom class name with the `className` prop');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* 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.
*/

describe('ToggletipLabel', () => {
describe('Component API', () => {
it.todo('should support custom elements with the `as` prop');
it.todo('should support a custom class name with the `className` prop');
});
});
252 changes: 252 additions & 0 deletions packages/react/src/components/Toggletip/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
/**
* 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 cx from 'classnames';
import PropTypes from 'prop-types';
import React, { useContext, useRef, useState } from 'react';
import { Popover, PopoverContent } from '../Popover';
import { match, keys } from '../../internal/keyboard';
import { useWindowEvent } from '../../internal/useEvent';
import { useId } from '../../internal/useId';
import { usePrefix } from '../../internal/usePrefix';

/**
* Used to render the label for a Toggletip
*/
function ToggletipLabel({
as: BaseComponent = 'span',
children,
className: customClassName,
}) {
const prefix = usePrefix();
const className = cx(`${prefix}--toggletip-label`, customClassName);
return <BaseComponent className={className}>{children}</BaseComponent>;
}

ToggletipLabel.propTypes = {
/**
* Provide a custom element or component to render the top-level node for the
* component.
*/
as: PropTypes.oneOfType([PropTypes.string, PropTypes.elementType]),

/**
* Custom children to be rendered as the content of the label
*/
children: PropTypes.node,

/**
* Provide a custom class name to be added to the outermost node in the
* component
*/
className: PropTypes.string,
};

// Used to coordinate accessibility props between button and content along with
// the actions to open and close the toggletip
const ToggletipContext = React.createContext();

function useToggletip() {
return useContext(ToggletipContext);
}

/**
* Used as a container for the button and content of a toggletip. This component
* is responsible for coordinating between interactions with the button and the
* visibility of the content
*/
function Toggletip({
align,
as,
className: customClassName,
children,
defaultOpen = false,
}) {
const ref = useRef();
const [open, setOpen] = useState(defaultOpen);
const prefix = usePrefix();
const id = useId();
const className = cx(`${prefix}--toggletip`, customClassName, {
[`${prefix}--toggletip--open`]: open,
});
const actions = {
toggle: () => {
setOpen(!open);
},
close: () => {
setOpen(false);
},
};
const value = {
buttonProps: {
'aria-expanded': open,
'aria-controls': id,
onClick: actions.toggle,
},
contentProps: {
id,
},
};

function onKeyDown(event) {
if (open && match(event, keys.Escape)) {
actions.close();
}
}

useWindowEvent('click', (event) => {
if (open && !ref.current.contains(event.target)) {
actions.close();
}
});

return (
<ToggletipContext.Provider value={value}>
<Popover
align={align}
as={as}
caret
className={className}
dropShadow={false}
highContrast
open={open}
onKeyDown={onKeyDown}
ref={ref}>
{children}
</Popover>
</ToggletipContext.Provider>
);
}

Toggletip.propTypes = {
/**
* Specify how the toggletip should align with the button
*/
align: PropTypes.oneOf(['top', 'bottom', 'left', 'right']),

/**
* Provide a custom element or component to render the top-level node for the
* component.
*/
as: PropTypes.oneOfType([PropTypes.string, PropTypes.elementType]),

/**
* Custom children to be rendered as the content of the label
*/
children: PropTypes.node,

/**
* Provide a custom class name to be added to the outermost node in the
* component
*/
className: PropTypes.string,

/**
* Specify if the toggletip should be open by default
*/
defaultOpen: PropTypes.bool,
};

/**
* `ToggletipButton` controls the visibility of the Toggletip through mouse
* clicks and keyboard interactions.
*/
function ToggletipButton({
children,
className: customClassName,
label = 'Show information',
}) {
const toggletip = useToggletip();
const prefix = usePrefix();
const className = cx(`${prefix}--toggletip-button`, customClassName);
return (
<button
{...toggletip.buttonProps}
aria-label={label}
type="button"
className={className}>
{children}
</button>
);
}

ToggletipButton.propTypes = {
/**
* Custom children to be rendered as the content of the label
*/
children: PropTypes.node,

/**
* Provide a custom class name to be added to the outermost node in the
* component
*/
className: PropTypes.string,

/**
* Provide an accessible label for this button
*/
label: PropTypes.string,
};

/**
* `ToggletipContent` is a wrapper around `PopoverContent`. It places the
* `children` passed in as a prop inside of `PopoverContent` so that they will
* be rendered inside of the popover for this component.
*/
function ToggletipContent({ children, className: customClassName }) {
const toggletip = useToggletip();
const prefix = usePrefix();
return (
<PopoverContent className={customClassName} {...toggletip.contentProps}>
<div className={`${prefix}--toggletip-content`}>{children}</div>
</PopoverContent>
);
}

ToggletipContent.propTypes = {
/**
* Custom children to be rendered as the content of the label
*/
children: PropTypes.node,

/**
* Provide a custom class name to be added to the outermost node in the
* component
*/
className: PropTypes.string,
};

/**
* `ToggletipActions` is a container for one or two actions present at the base
* of a toggletip. It is used for layout of these items.
*/
function ToggletipActions({ children, className: customClassName }) {
const prefix = usePrefix();
const className = cx(`${prefix}--toggletip-actions`, customClassName);
return <div className={className}>{children}</div>;
}

ToggletipActions.propTypes = {
/**
* Custom children to be rendered as the content of the label
*/
children: PropTypes.node,

/**
* Provide a custom class name to be added to the outermost node in the
* component
*/
className: PropTypes.string,
};

export {
ToggletipLabel,
Toggletip,
ToggletipButton,
ToggletipContent,
ToggletipActions,
};
29 changes: 29 additions & 0 deletions packages/react/src/components/Toggletip/next/Toggletip.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Preview, Props, Story } from '@storybook/addon-docs/blocks';

# Toggletip

[Source code](https://github.com/carbon-design-system/carbon/tree/main/packages/react/src/components/Toggletip)

<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->

## Table of Contents

- [Overview](#overview)
- [Component API](#component-api)
- [Feedback](#feedback)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

## Overview

## Component API

<Props />

## Feedback

Help us improve this component by providing feedback, asking questions on Slack,
or updating this file on
[GitHub](https://github.com/carbon-design-system/carbon/edit/main/packages/react/src/components/Toggletip/next/Toggletip.mdx).

Loading