mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 09:52:26 +01:00
Adding Label component (#16203)
* Adding Label component * doc fixes * Adding label wrapping input
This commit is contained in:
parent
3d37ad3b6e
commit
cda3e3e4c0
@ -10,6 +10,7 @@
|
|||||||
@import 'button-primary/button-primary';
|
@import 'button-primary/button-primary';
|
||||||
@import 'button-secondary/button-secondary';
|
@import 'button-secondary/button-secondary';
|
||||||
@import 'icon/icon';
|
@import 'icon/icon';
|
||||||
|
@import 'label/label';
|
||||||
@import 'tag/tag';
|
@import 'tag/tag';
|
||||||
@import 'text/text';
|
@import 'text/text';
|
||||||
@import 'text-field/text-field';
|
@import 'text-field/text-field';
|
||||||
|
95
ui/components/component-library/label/README.mdx
Normal file
95
ui/components/component-library/label/README.mdx
Normal file
@ -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>;
|
||||||
|
```
|
1
ui/components/component-library/label/index.js
Normal file
1
ui/components/component-library/label/index.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { Label } from './label';
|
73
ui/components/component-library/label/label.js
Normal file
73
ui/components/component-library/label/label.js
Normal file
@ -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,
|
||||||
|
};
|
11
ui/components/component-library/label/label.scss
Normal file
11
ui/components/component-library/label/label.scss
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
112
ui/components/component-library/label/label.stories.js
Normal file
112
ui/components/component-library/label/label.stories.js
Normal file
@ -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,
|
||||||
|
};
|
67
ui/components/component-library/label/label.test.js
Normal file
67
ui/components/component-library/label/label.test.js
Normal file
@ -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
Block a user