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