Adding Label component (#16203)
* Adding Label component * doc fixes * Adding label wrapping inputfeature/default_network_editable
parent
3d37ad3b6e
commit
cda3e3e4c0
@ -0,0 +1,95 @@ |
|||||||
|
import { Story, Canvas, ArgsTable } from '@storybook/addon-docs'; |
||||||
|
|
||||||
|
import { Label } from './label'; |
||||||
|
|
||||||
|
# Label |
||||||
|
|
||||||
|
The `Label` is a component used to label form inputs. |
||||||
|
|
||||||
|
<Canvas> |
||||||
|
<Story id="ui-components-component-library-label-label-stories-js--default-story" /> |
||||||
|
</Canvas> |
||||||
|
|
||||||
|
## Props |
||||||
|
|
||||||
|
The `Label` accepts all props below as well as all [Text](/docs/ui-components-component-library-text-text-stories-js--default-story#props) and [Box](/docs/ui-components-ui-box-box-stories-js--default-story#props) component props. |
||||||
|
|
||||||
|
<ArgsTable of={Label} /> |
||||||
|
|
||||||
|
### Children |
||||||
|
|
||||||
|
The `children` of the label can be text or a react node. |
||||||
|
|
||||||
|
<Canvas> |
||||||
|
<Story id="ui-components-component-library-label-label-stories-js--children" /> |
||||||
|
</Canvas> |
||||||
|
|
||||||
|
```jsx |
||||||
|
import { DISPLAY, ALIGN_ITEMS, FLEX_DIRECTION, SIZES, COLORS } from '../../../helpers/constants/design-system'; |
||||||
|
import { Icon, ICON_NAMES } from '../../ui/component-library/icon'; |
||||||
|
import { Label } from '../../ui/component-library/label'; |
||||||
|
import { TextFieldBase } from '../../ui/component-library/text-field-base' |
||||||
|
|
||||||
|
<Label>Plain text</Label> |
||||||
|
<Label display={DISPLAY.FLEX} alignItems={ALIGN_ITEMS.FLEX_START}> |
||||||
|
Text and icon |
||||||
|
<Icon |
||||||
|
color={COLORS.ICON_ALTERNATIVE} |
||||||
|
name={ICON_NAMES.INFO_FILLED} |
||||||
|
size={SIZES.AUTO} |
||||||
|
/> |
||||||
|
</Label> |
||||||
|
<Label |
||||||
|
display={DISPLAY.INLINE_FLEX} |
||||||
|
flexDirection={FLEX_DIRECTION.COLUMN} |
||||||
|
alignItems={ALIGN_ITEMS.FLEX_START} |
||||||
|
> |
||||||
|
Label that wraps an input |
||||||
|
{/* TODO: replace with TextField component */} |
||||||
|
<TextFieldBase placeholder="Click label to focus" /> |
||||||
|
</Label> |
||||||
|
``` |
||||||
|
|
||||||
|
### Html For |
||||||
|
|
||||||
|
Use the `htmlFor` prop to allow the `Label` to focus on an input with the same id when clicked. The cursor will also change to a `pointer` when the `htmlFor` has a value |
||||||
|
|
||||||
|
<Canvas> |
||||||
|
<Story id="ui-components-component-library-label-label-stories-js--html-for" /> |
||||||
|
</Canvas> |
||||||
|
|
||||||
|
```jsx |
||||||
|
import { TextFieldBase } from '../../ui/component-library/text-field-base'; |
||||||
|
import { Label } from '../../ui/component-library/label'; |
||||||
|
|
||||||
|
<Label htmlFor="add-network">Add network</Label> |
||||||
|
<TextFieldBase id="add-network" placeholder="Enter network name" /> |
||||||
|
``` |
||||||
|
|
||||||
|
### Required |
||||||
|
|
||||||
|
Use the `required` prop to add a required red asterisk next to the `children` of the `Label`. Note the required asterisk will always render after the `children`. |
||||||
|
|
||||||
|
<Canvas> |
||||||
|
<Story id="ui-components-component-library-label-label-stories-js--required" /> |
||||||
|
</Canvas> |
||||||
|
|
||||||
|
```jsx |
||||||
|
import { Label } from '../../ui/component-library/label'; |
||||||
|
|
||||||
|
<Label required>Label</Label>; |
||||||
|
``` |
||||||
|
|
||||||
|
### Disabled |
||||||
|
|
||||||
|
Use the `disabled` prop to set the `Label` in disabled state |
||||||
|
|
||||||
|
<Canvas> |
||||||
|
<Story id="ui-components-component-library-label-label-stories-js--disabled" /> |
||||||
|
</Canvas> |
||||||
|
|
||||||
|
```jsx |
||||||
|
import { Label } from '../../ui/component-library/label'; |
||||||
|
|
||||||
|
<Label disabled>Label</Label>; |
||||||
|
``` |
@ -0,0 +1 @@ |
|||||||
|
export { Label } from './label'; |
@ -0,0 +1,73 @@ |
|||||||
|
import React from 'react'; |
||||||
|
import PropTypes from 'prop-types'; |
||||||
|
import classnames from 'classnames'; |
||||||
|
|
||||||
|
import { |
||||||
|
COLORS, |
||||||
|
FONT_WEIGHT, |
||||||
|
TEXT, |
||||||
|
DISPLAY, |
||||||
|
ALIGN_ITEMS, |
||||||
|
} from '../../../helpers/constants/design-system'; |
||||||
|
import { Text } from '../text'; |
||||||
|
|
||||||
|
export const Label = ({ |
||||||
|
htmlFor, |
||||||
|
required, |
||||||
|
disabled, |
||||||
|
className, |
||||||
|
children, |
||||||
|
...props |
||||||
|
}) => ( |
||||||
|
<Text |
||||||
|
as="label" |
||||||
|
disabled={disabled} |
||||||
|
htmlFor={htmlFor} |
||||||
|
className={classnames( |
||||||
|
'mm-label', |
||||||
|
{ 'mm-label--disabled': disabled }, |
||||||
|
{ 'mm-label--html-for': htmlFor && !disabled }, |
||||||
|
className, |
||||||
|
)} |
||||||
|
variant={TEXT.BODY_MD} |
||||||
|
fontWeight={FONT_WEIGHT.BOLD} |
||||||
|
display={DISPLAY.INLINE_FLEX} |
||||||
|
alignItems={ALIGN_ITEMS.CENTER} |
||||||
|
{...props} |
||||||
|
> |
||||||
|
{children} |
||||||
|
{required && ( |
||||||
|
<Text |
||||||
|
as="span" |
||||||
|
className="mm-label__required-asterisk" |
||||||
|
aria-hidden="true" |
||||||
|
color={COLORS.ERROR_DEFAULT} |
||||||
|
> |
||||||
|
* |
||||||
|
</Text> |
||||||
|
)} |
||||||
|
</Text> |
||||||
|
); |
||||||
|
|
||||||
|
Label.propTypes = { |
||||||
|
/** |
||||||
|
* The content of the label |
||||||
|
*/ |
||||||
|
children: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), |
||||||
|
/** |
||||||
|
* The id of the input associated with the label |
||||||
|
*/ |
||||||
|
htmlFor: PropTypes.string, |
||||||
|
/** |
||||||
|
* If true the label will display as required |
||||||
|
*/ |
||||||
|
required: PropTypes.bool, |
||||||
|
/** |
||||||
|
* Whether the label is disabled or not |
||||||
|
*/ |
||||||
|
disabled: PropTypes.bool, |
||||||
|
/** |
||||||
|
* Additional classNames to be added to the label component |
||||||
|
*/ |
||||||
|
className: PropTypes.string, |
||||||
|
}; |
@ -0,0 +1,11 @@ |
|||||||
|
.mm-label { |
||||||
|
--label-opacity-disabled: 0.5; // TODO: replace with design token |
||||||
|
|
||||||
|
&--html-for { |
||||||
|
cursor: pointer; |
||||||
|
} |
||||||
|
|
||||||
|
&--disabled { |
||||||
|
opacity: var(--label-opacity-disabled); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,112 @@ |
|||||||
|
import React, { useState } from 'react'; |
||||||
|
import { |
||||||
|
DISPLAY, |
||||||
|
FLEX_DIRECTION, |
||||||
|
COLORS, |
||||||
|
SIZES, |
||||||
|
ALIGN_ITEMS, |
||||||
|
} from '../../../helpers/constants/design-system'; |
||||||
|
|
||||||
|
import Box from '../../ui/box'; |
||||||
|
import { Icon, ICON_NAMES } from '../icon'; |
||||||
|
import { TextFieldBase } from '../text-field-base'; |
||||||
|
|
||||||
|
import { Label } from './label'; |
||||||
|
|
||||||
|
import README from './README.mdx'; |
||||||
|
|
||||||
|
export default { |
||||||
|
title: 'Components/ComponentLibrary/Label', |
||||||
|
id: __filename, |
||||||
|
component: Label, |
||||||
|
parameters: { |
||||||
|
docs: { |
||||||
|
page: README, |
||||||
|
}, |
||||||
|
}, |
||||||
|
argTypes: { |
||||||
|
htmlFor: { |
||||||
|
control: 'text', |
||||||
|
}, |
||||||
|
required: { |
||||||
|
control: 'boolean', |
||||||
|
}, |
||||||
|
disabled: { |
||||||
|
control: 'boolean', |
||||||
|
}, |
||||||
|
children: { |
||||||
|
control: 'text', |
||||||
|
}, |
||||||
|
className: { |
||||||
|
control: 'text', |
||||||
|
}, |
||||||
|
}, |
||||||
|
args: { |
||||||
|
children: 'Label', |
||||||
|
}, |
||||||
|
}; |
||||||
|
|
||||||
|
const Template = (args) => <Label {...args} />; |
||||||
|
|
||||||
|
export const DefaultStory = Template.bind({}); |
||||||
|
DefaultStory.storyName = 'Default'; |
||||||
|
|
||||||
|
export const Children = (args) => ( |
||||||
|
<Box |
||||||
|
display={DISPLAY.INLINE_FLEX} |
||||||
|
flexDirection={FLEX_DIRECTION.COLUMN} |
||||||
|
gap={2} |
||||||
|
> |
||||||
|
<Label {...args}>Plain text</Label> |
||||||
|
<Label {...args} display={DISPLAY.FLEX} alignItems={ALIGN_ITEMS.FLEX_START}> |
||||||
|
Text and icon |
||||||
|
<Icon |
||||||
|
color={COLORS.ICON_ALTERNATIVE} |
||||||
|
name={ICON_NAMES.INFO_FILLED} |
||||||
|
size={SIZES.AUTO} |
||||||
|
/> |
||||||
|
</Label> |
||||||
|
<Label |
||||||
|
{...args} |
||||||
|
display={DISPLAY.INLINE_FLEX} |
||||||
|
flexDirection={FLEX_DIRECTION.COLUMN} |
||||||
|
alignItems={ALIGN_ITEMS.FLEX_START} |
||||||
|
> |
||||||
|
Label that wraps an input |
||||||
|
{/* TODO: replace with TextField component */} |
||||||
|
<TextFieldBase placeholder="Click label to focus" /> |
||||||
|
</Label> |
||||||
|
</Box> |
||||||
|
); |
||||||
|
|
||||||
|
export const HtmlFor = (args) => { |
||||||
|
const [value, setValue] = useState(''); |
||||||
|
const handleOnChange = (e) => { |
||||||
|
setValue(e.target.value); |
||||||
|
}; |
||||||
|
return ( |
||||||
|
<Box display={DISPLAY.INLINE_FLEX} flexDirection={FLEX_DIRECTION.COLUMN}> |
||||||
|
<Label {...args} /> |
||||||
|
<TextFieldBase |
||||||
|
id="add-network" |
||||||
|
value={value} |
||||||
|
onChange={handleOnChange} |
||||||
|
placeholder="Enter network name" |
||||||
|
/> |
||||||
|
</Box> |
||||||
|
); |
||||||
|
}; |
||||||
|
HtmlFor.args = { |
||||||
|
children: 'Network name', |
||||||
|
htmlFor: 'add-network', |
||||||
|
}; |
||||||
|
|
||||||
|
export const Required = Template.bind({}); |
||||||
|
Required.args = { |
||||||
|
required: true, |
||||||
|
}; |
||||||
|
|
||||||
|
export const Disabled = Template.bind({}); |
||||||
|
Disabled.args = { |
||||||
|
disabled: true, |
||||||
|
}; |
@ -0,0 +1,67 @@ |
|||||||
|
/* eslint-disable jest/require-top-level-describe */ |
||||||
|
import { fireEvent, render } from '@testing-library/react'; |
||||||
|
import React from 'react'; |
||||||
|
import { Icon, ICON_NAMES } from '../icon'; |
||||||
|
import { TextFieldBase } from '../text-field-base'; |
||||||
|
|
||||||
|
import { Label } from './label'; |
||||||
|
|
||||||
|
describe('label', () => { |
||||||
|
it('should render text inside the label', () => { |
||||||
|
const { getByText } = render(<Label>label</Label>); |
||||||
|
expect(getByText('label')).toBeDefined(); |
||||||
|
}); |
||||||
|
it('should render text and react nodes as children', () => { |
||||||
|
const { getByText, getByTestId } = render( |
||||||
|
<Label> |
||||||
|
label |
||||||
|
<Icon name={ICON_NAMES.INFO_FILLED} data-testid="icon" /> |
||||||
|
</Label>, |
||||||
|
); |
||||||
|
expect(getByText('label')).toBeDefined(); |
||||||
|
expect(getByTestId('icon')).toBeDefined(); |
||||||
|
}); |
||||||
|
it('should be able to accept an htmlFor prop and focus an input of a given id', () => { |
||||||
|
const { getByText, getByRole } = render( |
||||||
|
<> |
||||||
|
<Label htmlFor="input">label</Label> |
||||||
|
<TextFieldBase id="input" /> |
||||||
|
</>, |
||||||
|
); |
||||||
|
const input = getByRole('textbox'); |
||||||
|
const label = getByText('label'); |
||||||
|
expect(label).toBeDefined(); |
||||||
|
expect(input).not.toHaveFocus(); |
||||||
|
fireEvent.click(label); |
||||||
|
expect(input).toHaveFocus(); |
||||||
|
}); |
||||||
|
it('should render when wrapping an input and focus input when clicked without htmlFor', () => { |
||||||
|
const { getByText, getByRole } = render( |
||||||
|
<> |
||||||
|
<Label> |
||||||
|
Label text |
||||||
|
<TextFieldBase /> |
||||||
|
</Label> |
||||||
|
</>, |
||||||
|
); |
||||||
|
const input = getByRole('textbox'); |
||||||
|
const label = getByText('Label text'); |
||||||
|
expect(label).toBeDefined(); |
||||||
|
expect(input).not.toHaveFocus(); |
||||||
|
fireEvent.click(label); |
||||||
|
expect(input).toHaveFocus(); |
||||||
|
}); |
||||||
|
it('should render with required asterisk', () => { |
||||||
|
const { getByText } = render(<Label required>label</Label>); |
||||||
|
expect(getByText('label')).toBeDefined(); |
||||||
|
expect(getByText('*')).toBeDefined(); |
||||||
|
}); |
||||||
|
it('should render with disabled state and have disabled class', () => { |
||||||
|
const { getByText } = render(<Label disabled>label</Label>); |
||||||
|
expect(getByText('label')).toHaveClass('mm-label--disabled'); |
||||||
|
}); |
||||||
|
it('should render with additional className', () => { |
||||||
|
const { getByText } = render(<Label className="test-class">label</Label>); |
||||||
|
expect(getByText('label')).toHaveClass('test-class'); |
||||||
|
}); |
||||||
|
}); |
Loading…
Reference in new issue