1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 09:52:26 +01:00

Feat/add header base component (#18043)

* add header base component

* fix resizing issue

* add center

* add demo

* header base using flexbox

* fix button issue

* header base clean up

* update tests

* add readme description

* add docs

* update snapshot

* add more to readme

* convert to TS

* fix file name

* fix types and colors

* fix classname error

* fix boxprops import

* fix boxprops

* prop fix

* fix errors

* Update ui/components/component-library/header-base/header-base.stories.tsx

Co-authored-by: George Marshall <george.marshall@consensys.net>

* Update ui/components/component-library/header-base/header-base.types.ts

Co-authored-by: George Marshall <george.marshall@consensys.net>

* Update ui/components/component-library/header-base/header-base.types.ts

Co-authored-by: George Marshall <george.marshall@consensys.net>

* headerbase fixes

* fix export

* remove Math.max

* change order for index on storybook to prep build

* revert back to order

* remove type from export

* add type to export

* change export of headerbase function

* export update

* revert back to normal

* add type to export

* Removing interface export from index

---------

Co-authored-by: George Marshall <george.marshall@consensys.net>
This commit is contained in:
Garrett Bear 2023-03-23 08:24:23 -07:00 committed by GitHub
parent e424debfc0
commit ed5b78d61b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 638 additions and 0 deletions

View File

@ -0,0 +1,114 @@
import { Story, Canvas, ArgsTable } from '@storybook/addon-docs';
import { HeaderBase } from './header-base';
### This is a base component. It should not be used in your feature code directly but as a "base" for other UI components
# HeaderBase
The `HeaderBase` component is a reusable UI component for displaying a header with optional startAccessory, children (title) and endAccessory content areas. It is designed to be flexible and customizable for various use cases to keep a visually balanced appearance.
<Canvas>
<Story id="components-componentlibrary-headerbase--default-story" />
</Canvas>
## Props
The `HeaderBase` accepts all props below as well as all [Box](/docs/components-ui-box--default-story#props) component props
<ArgsTable of={HeaderBase} />
### Children
Wrapping content in the `HeaderBase` component will be rendered in the center of the header.
Use the `childrenWrapperProps` prop to customize the wrapper element around the `children` content.
<Canvas>
<Story id="components-componentlibrary-headerbase--children" />
</Canvas>
```jsx
import { HeaderBase, Text } from '../../component-library';
import {
TEXT_ALIGN,
TextVariant,
} from '../../../helpers/constants/design-system';
<HeaderBase>
<Text variant={TextVariant.headingSm} textAlign={TEXT_ALIGN.CENTER}>
Title is sentence case no period
</Text>
</HeaderBase>;
```
### startAccessory
Using the `startAccessory` prop will render the content in the start (left) side of the header.
Use the `startAccessoryWrapperProps` prop to customize the wrapper element around the `startAccessory` content.
<Canvas>
<Story id="components-componentlibrary-headerbase--start-accessory" />
</Canvas>
```jsx
import { HeaderBase, Text } from '../../component-library';
import {
TEXT_ALIGN,
TextVariant,
} from '../../../helpers/constants/design-system';
<HeaderBase
startAccessory={
<ButtonIcon
size={BUTTON_ICON_SIZES.SM}
iconName={ICON_NAMES.ARROW_LEFT}
ariaLabel="back"
/>
}
>
<Text variant={TextVariant.headingSm} textAlign={TEXT_ALIGN.CENTER}>
Title is sentence case no period
</Text>
</HeaderBase>;
```
### endAccessory
Using the `endAccessory` prop will render the content in the end (right) side of the header.
Use the `endAccessoryWrapperProps` prop to customize the wrapper element around the `endAccessory` content.
<Canvas>
<Story id="components-componentlibrary-headerbase--end-accessory" />
</Canvas>
```jsx
import { HeaderBase, Text } from '../../component-library';
import {
TEXT_ALIGN,
TextVariant,
} from '../../../helpers/constants/design-system';
<HeaderBase
endAccessory={
<ButtonIcon
size={BUTTON_ICON_SIZES.SM}
iconName={ICON_NAMES.CLOSE}
ariaLabel="close"
/>
}
>
<Text variant={TextVariant.headingSm} textAlign={TEXT_ALIGN.CENTER}>
Title is sentence case no period
</Text>
</HeaderBase>;
```
### Use Case Demos
Some examples of how the `HeaderBase` component can be used in various use cases with background colors set for visual aid.
<Canvas>
<Story id="components-componentlibrary-headerbase--use-case-demos" />
</Canvas>

View File

@ -0,0 +1,17 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`HeaderBase should render HeaderBase element correctly 1`] = `
<div>
<div
class="box mm-header-base box--display-flex box--flex-direction-row box--justify-content-space-between"
data-testid="header-base"
title="HeaderBase test"
>
<div
class="box mm-header-base__children box--flex-direction-row box--width-full"
>
should render HeaderBase element correctly
</div>
</div>
</div>
`;

View File

@ -0,0 +1,293 @@
import React from 'react';
import { ComponentStory, ComponentMeta } from '@storybook/react';
import Box from '../../ui/box';
import {
ICON_NAMES,
Button,
ButtonIcon,
BUTTON_ICON_SIZES,
BUTTON_SIZES,
Text,
} from '..';
import {
AlignItems,
BackgroundColor,
TextVariant,
TEXT_ALIGN,
} from '../../../helpers/constants/design-system';
import { HeaderBase } from './header-base';
import README from './README.mdx';
export default {
title: 'Components/ComponentLibrary/HeaderBase',
component: HeaderBase,
parameters: {
docs: {
page: README,
},
},
} as ComponentMeta<typeof HeaderBase>;
const Template: ComponentStory<typeof HeaderBase> = (args) => (
<HeaderBase {...args} />
);
export const DefaultStory = Template.bind({});
DefaultStory.args = {
children: (
<Text variant={TextVariant.headingSm} textAlign={TEXT_ALIGN.CENTER}>
Title is sentence case no period
</Text>
),
startAccessory: (
<ButtonIcon
size={BUTTON_ICON_SIZES.SM}
iconName={ICON_NAMES.ARROW_LEFT}
ariaLabel="back"
/>
),
endAccessory: (
<ButtonIcon
size={BUTTON_ICON_SIZES.SM}
iconName={ICON_NAMES.CLOSE}
ariaLabel="close"
/>
),
};
DefaultStory.storyName = 'Default';
export const Children = (args) => {
return (
<HeaderBase {...args}>
<Text variant={TextVariant.headingSm} textAlign={TEXT_ALIGN.CENTER}>
Title is sentence case no period
</Text>
</HeaderBase>
);
};
export const StartAccessory = (args) => {
return (
<HeaderBase
marginBottom={4}
startAccessory={
<ButtonIcon
size={BUTTON_ICON_SIZES.SM}
iconName={ICON_NAMES.ARROW_LEFT}
ariaLabel="back"
/>
}
{...args}
>
<Text variant={TextVariant.headingSm} textAlign={TEXT_ALIGN.CENTER}>
Title is sentence case no period
</Text>
</HeaderBase>
);
};
export const EndAccessory = (args) => {
return (
<HeaderBase
marginBottom={4}
endAccessory={
<ButtonIcon
size={BUTTON_ICON_SIZES.SM}
iconName={ICON_NAMES.CLOSE}
ariaLabel="close"
/>
}
{...args}
>
<Text variant={TextVariant.headingSm} textAlign={TEXT_ALIGN.CENTER}>
Title is sentence case no period
</Text>
</HeaderBase>
);
};
export const UseCaseDemos = (args) => (
<>
<Text>children only assigned</Text>
<Box backgroundColor={BackgroundColor.warningAlternative}>
<HeaderBase marginBottom={4} {...args}>
<Text
variant={TextVariant.headingSm}
textAlign={TEXT_ALIGN.CENTER}
backgroundColor={BackgroundColor.primaryAlternative}
>
Title is sentence case no period
</Text>
</HeaderBase>
</Box>
<Text>children and endAccessory assigned </Text>
<Box backgroundColor={BackgroundColor.warningAlternative}>
<HeaderBase
marginBottom={4}
endAccessory={
<ButtonIcon
backgroundColor={BackgroundColor.goerli}
size={BUTTON_ICON_SIZES.SM}
iconName={ICON_NAMES.CLOSE}
ariaLabel="close"
/>
}
{...args}
>
<Text
variant={TextVariant.headingSm}
textAlign={TEXT_ALIGN.CENTER}
backgroundColor={BackgroundColor.primaryAlternative}
>
Title is sentence case no period
</Text>
</HeaderBase>
</Box>
<Text>children and startAccessory assigned </Text>
<Box backgroundColor={BackgroundColor.warningAlternative}>
<HeaderBase
marginBottom={4}
startAccessory={
<ButtonIcon
backgroundColor={BackgroundColor.successAlternative}
size={BUTTON_ICON_SIZES.SM}
iconName={ICON_NAMES.ARROW_LEFT}
ariaLabel="back"
/>
}
{...args}
>
<Text
variant={TextVariant.headingSm}
textAlign={TEXT_ALIGN.CENTER}
backgroundColor={BackgroundColor.primaryAlternative}
>
Title is sentence case no period
</Text>
</HeaderBase>
</Box>
<Text>children, startAccessory, and endAccessory assigned </Text>
<Box backgroundColor={BackgroundColor.warningAlternative}>
<HeaderBase
marginBottom={4}
startAccessory={
<ButtonIcon
backgroundColor={BackgroundColor.successAlternative}
size={BUTTON_ICON_SIZES.SM}
iconName={ICON_NAMES.ARROW_LEFT}
ariaLabel="back"
/>
}
endAccessory={
<ButtonIcon
backgroundColor={BackgroundColor.goerli}
size={BUTTON_ICON_SIZES.SM}
iconName={ICON_NAMES.CLOSE}
ariaLabel="close"
/>
}
{...args}
>
<Text
variant={TextVariant.headingSm}
textAlign={TEXT_ALIGN.CENTER}
backgroundColor={BackgroundColor.primaryAlternative}
>
Title is sentence case no period
</Text>
</HeaderBase>
</Box>
<Text>children, startAccessory, and endAccessory assigned </Text>
<Box backgroundColor={BackgroundColor.warningAlternative}>
<HeaderBase
marginBottom={4}
startAccessory={
<Button
backgroundColor={BackgroundColor.successAlternative}
style={{ whiteSpace: 'nowrap' }}
size={BUTTON_SIZES.SM}
>
Unlock Now
</Button>
}
endAccessory={
<ButtonIcon
backgroundColor={BackgroundColor.goerli}
size={BUTTON_ICON_SIZES.SM}
iconName={ICON_NAMES.CLOSE}
ariaLabel="close"
/>
}
{...args}
>
<Text
variant={TextVariant.headingSm}
textAlign={TEXT_ALIGN.CENTER}
backgroundColor={BackgroundColor.primaryAlternative}
>
Title is sentence case no period
</Text>
</HeaderBase>
</Box>
<Text>
children, startAccessory, and endAccessory assigned with prop alignItems=
{AlignItems.center} passed at HeaderBase
</Text>
<Box backgroundColor={BackgroundColor.warningAlternative}>
<HeaderBase
marginBottom={4}
alignItems={AlignItems.center}
startAccessory={
<ButtonIcon
backgroundColor={BackgroundColor.successAlternative}
size={BUTTON_ICON_SIZES.SM}
iconName={ICON_NAMES.CLOSE}
ariaLabel="close"
/>
}
endAccessory={
<Button
backgroundColor={BackgroundColor.goerli}
size={BUTTON_SIZES.SM}
>
Download
</Button>
}
{...args}
>
<Text
variant={TextVariant.headingSm}
textAlign={TEXT_ALIGN.CENTER}
backgroundColor={BackgroundColor.primaryAlternative}
>
Title is sentence case no period
</Text>
</HeaderBase>
</Box>
<Text>startAccessory and endAccessory assigned </Text>
<Box backgroundColor={BackgroundColor.warningAlternative}>
<HeaderBase
marginBottom={4}
startAccessory={
<Button
backgroundColor={BackgroundColor.successAlternative}
size={BUTTON_SIZES.SM}
>
Unlock
</Button>
}
endAccessory={
<ButtonIcon
backgroundColor={BackgroundColor.goerli}
size={BUTTON_ICON_SIZES.SM}
iconName={ICON_NAMES.CLOSE}
ariaLabel="close"
/>
}
{...args}
></HeaderBase>
</Box>
</>
);

View File

@ -0,0 +1,61 @@
/* eslint-disable jest/require-top-level-describe */
import { render } from '@testing-library/react';
import React from 'react';
import { Icon, ICON_NAMES } from '..';
import { HeaderBase } from './header-base';
describe('HeaderBase', () => {
it('should render HeaderBase element correctly', () => {
const { getByTestId, container } = render(
<HeaderBase data-testid="header-base" title="HeaderBase test">
should render HeaderBase element correctly
</HeaderBase>,
);
expect(getByTestId('header-base')).toHaveClass('mm-header-base');
expect(container).toMatchSnapshot();
});
it('should render with added classname', () => {
const { getByTestId } = render(
<HeaderBase
className="mm-header-base--test"
data-testid="header-base"
title="HeaderBase test"
>
should render HeaderBase element correctly
</HeaderBase>,
);
expect(getByTestId('header-base')).toHaveClass('mm-header-base--test');
});
it('should render HeaderBase children', () => {
const { getByText } = render(
<HeaderBase>HeaderBase children test</HeaderBase>,
);
expect(getByText('HeaderBase children test')).toBeDefined();
});
it('should render HeaderBase startAccessory', () => {
const { getByTestId } = render(
<HeaderBase
startAccessory={
<Icon data-testid="start-accessory" name={ICON_NAMES.ADD_SQUARE} />
}
/>,
);
expect(getByTestId('start-accessory')).toBeDefined();
});
it('should render HeaderBase endAccessory', () => {
const { getByTestId } = render(
<HeaderBase
endAccessory={
<Icon data-testid="end-accessory" name={ICON_NAMES.ADD_SQUARE} />
}
/>,
);
expect(getByTestId('end-accessory')).toBeDefined();
});
});

View File

@ -0,0 +1,117 @@
import React, { useRef, useLayoutEffect, useMemo, useState } from 'react';
import classnames from 'classnames';
import {
BLOCK_SIZES,
DISPLAY,
JustifyContent,
} from '../../../helpers/constants/design-system';
import Box from '../../ui/box';
import { HeaderBaseProps } from './header-base.types';
export const HeaderBase: React.FC<HeaderBaseProps> = ({
startAccessory,
endAccessory,
className = '',
children,
childrenWrapperProps,
startAccessoryWrapperProps,
endAccessoryWrapperProps,
...props
}) => {
const startAccessoryRef = useRef<HTMLDivElement>(null);
const endAccessoryRef = useRef<HTMLDivElement>(null);
const [accessoryMinWidth, setAccessoryMinWidth] = useState<number>();
useLayoutEffect(() => {
function handleResize() {
if (startAccessoryRef.current && endAccessoryRef.current) {
const accMinWidth = Math.max(
startAccessoryRef.current.scrollWidth,
endAccessoryRef.current.scrollWidth,
);
setAccessoryMinWidth(accMinWidth);
} else if (startAccessoryRef.current && !endAccessoryRef.current) {
setAccessoryMinWidth(startAccessoryRef.current.scrollWidth);
} else if (!startAccessoryRef.current && endAccessoryRef.current) {
setAccessoryMinWidth(endAccessoryRef.current.scrollWidth);
} else {
setAccessoryMinWidth(0);
}
}
handleResize();
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, [startAccessoryRef, endAccessoryRef, children]);
const getTitleStyles = useMemo(() => {
if (startAccessory && !endAccessory) {
return {
marginRight: `${accessoryMinWidth}px`,
};
} else if (!startAccessory && endAccessory) {
return {
marginLeft: `${accessoryMinWidth}px`,
};
}
return {};
}, [accessoryMinWidth, startAccessory, endAccessory]);
return (
<Box
className={classnames('mm-header-base', className)}
display={DISPLAY.FLEX}
justifyContent={JustifyContent.spaceBetween}
{...props}
>
{startAccessory && (
<Box
className="mm-header-base__start-accessory"
ref={startAccessoryRef}
style={
children
? {
minWidth: `${accessoryMinWidth}px`,
}
: undefined
}
{...startAccessoryWrapperProps}
>
{startAccessory}
</Box>
)}
{children && (
<Box
className="mm-header-base__children"
width={BLOCK_SIZES.FULL}
style={getTitleStyles}
{...childrenWrapperProps}
>
{children}
</Box>
)}
{endAccessory && (
<Box
display={DISPLAY.FLEX}
justifyContent={JustifyContent.flexEnd}
className="mm-header-base__end-accessory"
ref={endAccessoryRef}
style={
children
? {
minWidth: `${accessoryMinWidth}px`,
}
: undefined
}
{...endAccessoryWrapperProps}
>
{endAccessory}
</Box>
)}
</Box>
);
};

View File

@ -0,0 +1,33 @@
import React from 'react';
import type { BoxProps } from '../../ui/box/box.d';
export interface HeaderBaseProps extends BoxProps {
/**
* The children is the title area of the HeaderBase
*/
children?: React.ReactNode;
/**
* Use the `childrenWrapperProps` prop to define the props to the children wrapper
*/
childrenWrapperProps?: BoxProps;
/**
* The start(default left) content area of HeaderBase
*/
startAccessory?: React.ReactNode;
/**
* Use the `startAccessoryWrapperProps` prop to define the props to the start accessory wrapper
*/
startAccessoryWrapperProps?: BoxProps;
/**
* The end (default right) content area of HeaderBase
*/
endAccessory?: React.ReactNode;
/**
* Use the `endAccessoryWrapperProps` prop to define the props to the end accessory wrapper
*/
endAccessoryWrapperProps?: BoxProps;
/**
* An additional className to apply to the HeaderBase
*/
className?: string;
}

View File

@ -0,0 +1,2 @@
export { HeaderBase } from './header-base';
export type { HeaderBaseProps } from './header-base.types';

View File

@ -21,6 +21,7 @@ export { ButtonLink, BUTTON_LINK_SIZES } from './button-link';
export { ButtonPrimary, BUTTON_PRIMARY_SIZES } from './button-primary';
export { ButtonSecondary, BUTTON_SECONDARY_SIZES } from './button-secondary';
export { FormTextField } from './form-text-field';
export { HeaderBase } from './header-base';
export { HelpText } from './help-text';
export { Icon, ICON_NAMES, ICON_SIZES } from './icon';
export { Label } from './label';