diff --git a/.storybook/preview.js b/.storybook/preview.js index c807e83697..8c491b6775 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -74,7 +74,11 @@ addParameters({ "*", "Accessibility", "Hooks" - ] + ], + includeName: false, + // currently there is a bug that makes any componet stories to be order alphabetical even so + // it is not the default settings. This settings purpose is to sort all the stories of any component by their load time + storySort: (a, b) => 0 } } }); diff --git a/src/components/Flex/Flex.jsx b/src/components/Flex/Flex.jsx new file mode 100644 index 0000000000..5187ffacdf --- /dev/null +++ b/src/components/Flex/Flex.jsx @@ -0,0 +1,88 @@ +import React, { useRef, forwardRef, useMemo } from "react"; +import PropTypes from "prop-types"; +import cx from "classnames"; +import useMergeRefs from "../../hooks/useMergeRefs"; +import { FLEX_POSITIONS, FLEX_GAPS, FLEX_DIRECTIONS } from "./FlexConstants"; +import { BASE_POSITIONS } from "../../constants/positions"; +import Clickable from "../Clickable/Clickable"; +import classes from "./Flex.module.scss"; + +const Flex = forwardRef( + ({ className, id, elementType, direction, wrap, children, justify, align, gap, onClick, style }, ref) => { + const componentRef = useRef(null); + const mergedRef = useMergeRefs({ refs: [ref, componentRef] }); + const overrideStyle = useMemo(() => ({ ...style, gap: `${gap}px` }), [style, gap]); + const Element = onClick ? Clickable : elementType; + + return ( + + {children} + + ); + } +); + +Flex.justify = FLEX_POSITIONS; +Flex.align = BASE_POSITIONS; +Flex.gaps = FLEX_GAPS; +Flex.directions = FLEX_DIRECTIONS; + +Flex.propTypes = { + /** + * class name to be add to the wrapper + */ + className: PropTypes.string, + /** + * id to be add to the wrapper + */ + id: PropTypes.string, + style: PropTypes.object, + direction: PropTypes.oneOf([Flex.directions.ROW, Flex.directions.COLUMN]), + elementType: PropTypes.string, + wrap: PropTypes.bool, + children: PropTypes.oneOfType([PropTypes.element, PropTypes.arrayOf(PropTypes.element)]), + justify: PropTypes.oneOf([ + Flex.justify.START, + Flex.justify.CENTER, + Flex.justify.END, + Flex.justify.SPACE_BETWEEN, + Flex.justify.SPACE_AROUND + ]), + align: PropTypes.oneOf([Flex.align.START, Flex.align.CENTER, Flex.align.END]), + gap: PropTypes.oneOfType([ + PropTypes.oneOf([Flex.gaps.NONE, Flex.gaps.SMALL, Flex.gaps.MEDIUM, Flex.gaps.LARGE]), + PropTypes.number + ]) +}; + +Flex.defaultProps = { + className: "", + id: undefined, + elementType: "div", + style: undefined, + wrap: false, + children: undefined, + direction: Flex.directions.ROW, + justify: Flex.justify.START, + align: Flex.align.CENTER, + gap: Flex.gaps.NONE +}; + +export default Flex; diff --git a/src/components/Flex/Flex.module.scss b/src/components/Flex/Flex.module.scss new file mode 100644 index 0000000000..4b774dfe88 --- /dev/null +++ b/src/components/Flex/Flex.module.scss @@ -0,0 +1,48 @@ +@import "../../styles/themes.scss"; + +.container { + display: flex; + flex-direction: row; + &.justify { + &Start { + justify-content: flex-start; + } + + &End { + justify-content: flex-end; + } + + &Center { + justify-content: center; + } + + &SpaceBetween { + justify-content: space-between; + } + + &SpaceAround { + justify-content: space-around; + } + } + &.align { + &Start { + align-items: flex-start; + } + + &End { + align-items: flex-end; + } + + &Center { + align-items: center; + } + } + &.direction { + &Column { + flex-direction: column; + } + } + &.wrap { + flex-wrap: wrap; + } +} \ No newline at end of file diff --git a/src/components/Flex/FlexConstants.js b/src/components/Flex/FlexConstants.js new file mode 100644 index 0000000000..f725d6c643 --- /dev/null +++ b/src/components/Flex/FlexConstants.js @@ -0,0 +1,20 @@ +import { BASE_POSITIONS } from "../../constants/positions"; + +export const FLEX_POSITIONS = Object.freeze({ + ...BASE_POSITIONS, + SPACE_AROUND: "SpaceAround", + SPACE_BETWEEN: "SpaceBetween" +}); + +export const FLEX_GAPS = Object.freeze({ + XS: 4, + SMALL: 8, + MEDIUM: 16, + LARGE: 24, + NONE: 0 +}); + +export const FLEX_DIRECTIONS = Object.freeze({ + ROW: "Row", + COLUMN: "Column" +}); diff --git a/src/components/Flex/__stories__/Flex.stories.mdx b/src/components/Flex/__stories__/Flex.stories.mdx new file mode 100644 index 0000000000..e43c12dfdb --- /dev/null +++ b/src/components/Flex/__stories__/Flex.stories.mdx @@ -0,0 +1,299 @@ +import Flex from "../Flex"; +import { ArgsTable, Story, Canvas, Meta } from "@storybook/addon-docs"; +import { Add, Search, Person, Filter, Sort } from "../../Icon/Icons"; +import Button from "../../Button/Button"; +import classes from "./Flex.stories.module.scss"; +import Chips from "../../Chips/Chips"; +import { StoryDescription } from "../../../storybook/components/story-description/story-description"; +import { LIST, MENU, TABS } from "../../../storybook/components/related-components/component-description-map"; + + + + + +export const flexTemplate = (args) => { + return + + + + +} + + + +# Flex +- [Overview](#overview) +- [Props](#props) +- [Usage](#usage) +- [Variants](#variants) +- [Do’s and don’ts](#dos-and-donts) +- [Use cases and examples](#use-cases-and-examples) +- [Related components](#related-components) +- [Feedback](#feedback) + +## Overview +Use Flex component to position group of sub-elements in one dimension, horizontal or vertical, without being dependent on a custom CSS file for positioning the sub-elements. + + + + { flexTemplate.bind({}) } + + + +## Props + + +## Usage + + +## Variants +### Directions + + +
+ + + + + + + + + + + + + + +
+
+
+ +### Horizontal spacing between items + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +### Vertical spacing between items + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +### Horizontal positions + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +### Vertical positions + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +### Support multi lines layout +You can display a layout that includes multiple lines using the flex component wrap mode. +This mode allows the layout to break into multiple lines if all the component children cannot fit into one only. + + + + + + + + + + + + + + +## Use cases and examples + +### Flex as toolbar container +You can use flex component for create responsive toolbars + + + + + + + + + + + + + +## Related components + \ No newline at end of file diff --git a/src/components/Flex/__stories__/Flex.stories.module.scss b/src/components/Flex/__stories__/Flex.stories.module.scss new file mode 100644 index 0000000000..4d14119536 --- /dev/null +++ b/src/components/Flex/__stories__/Flex.stories.module.scss @@ -0,0 +1,15 @@ +@import "../../../styles/themes.scss"; + +.multi-lines-story-search { + width: 200px; +} + +.flex-chip { + margin: 0; +} + +.story-container { + & > * { + margin-bottom: var(--spacing-large); + } +} \ No newline at end of file diff --git a/src/components/Flex/__tests__/__snapshots__/flex-snapshot-tests.jest.js.snap b/src/components/Flex/__tests__/__snapshots__/flex-snapshot-tests.jest.js.snap new file mode 100644 index 0000000000..4abd4b8f24 --- /dev/null +++ b/src/components/Flex/__tests__/__snapshots__/flex-snapshot-tests.jest.js.snap @@ -0,0 +1,233 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Flex renders correctly Horizontal display with align 1`] = ` +
+
+ 1 +
+
+ 2 +
+
+ 3 +
+
+`; + +exports[`Flex renders correctly Horizontal display with align 2`] = ` +
+
+ 1 +
+
+ 2 +
+
+ 3 +
+
+`; + +exports[`Flex renders correctly Horizontal display with children 1`] = ` +
+
+ 1 +
+
+ 2 +
+
+ 3 +
+
+`; + +exports[`Flex renders correctly Horizontal display with gap 1`] = ` +
+
+ 1 +
+
+ 2 +
+
+ 3 +
+
+`; + +exports[`Flex renders correctly Horizontal display with justify 1`] = ` +
+
+ 1 +
+
+ 2 +
+
+ 3 +
+
+`; + +exports[`Flex renders correctly Horizontal display with wrap 1`] = ` +
+
+ 1 +
+
+ 2 +
+
+ 3 +
+
+`; + +exports[`Flex renders correctly Vertical display with align 1`] = ` +
+
+ 1 +
+
+ 2 +
+
+ 3 +
+
+`; + +exports[`Flex renders correctly Vertical display with children 1`] = ` +
+
+ 1 +
+
+ 2 +
+
+ 3 +
+
+`; + +exports[`Flex renders correctly Vertical display with justify 1`] = ` +
+
+ 1 +
+
+ 2 +
+
+ 3 +
+
+`; + +exports[`Flex renders correctly Vertical display with wrap 1`] = ` +
+
+ 1 +
+
+ 2 +
+
+ 3 +
+
+`; + +exports[`Flex renders correctly with empty props 1`] = ` +
+`; diff --git a/src/components/Flex/__tests__/flex-snapshot-tests.jest.js b/src/components/Flex/__tests__/flex-snapshot-tests.jest.js new file mode 100644 index 0000000000..d7cb58cde0 --- /dev/null +++ b/src/components/Flex/__tests__/flex-snapshot-tests.jest.js @@ -0,0 +1,135 @@ +import React from "react"; +import renderer from "react-test-renderer"; +import Flex from "../Flex"; + +describe("Flex renders correctly", () => { + it("with empty props", () => { + const tree = renderer.create().toJSON(); + expect(tree).toMatchSnapshot(); + }); + describe("Horizontal display", () => { + it("with children", () => { + const tree = renderer + .create( + +
1
+
2
+
3
+
+ ) + .toJSON(); + expect(tree).toMatchSnapshot(); + }); + it("with align", () => { + const tree = renderer + .create( + +
1
+
2
+
3
+
+ ) + .toJSON(); + expect(tree).toMatchSnapshot(); + }); + it("with justify", () => { + const tree = renderer + .create( + +
1
+
2
+
3
+
+ ) + .toJSON(); + expect(tree).toMatchSnapshot(); + }); + it("with align", () => { + const tree = renderer + .create( + +
1
+
2
+
3
+
+ ) + .toJSON(); + expect(tree).toMatchSnapshot(); + }); + it("with gap", () => { + const tree = renderer + .create( + +
1
+
2
+
3
+
+ ) + .toJSON(); + expect(tree).toMatchSnapshot(); + }); + it("with wrap", () => { + const tree = renderer + .create( + +
1
+
2
+
3
+
+ ) + .toJSON(); + expect(tree).toMatchSnapshot(); + }); + }); + describe("Vertical display", () => { + it("with children", () => { + const tree = renderer + .create( + +
1
+
2
+
3
+
+ ) + .toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it("with justify", () => { + const tree = renderer + .create( + +
1
+
2
+
3
+
+ ) + .toJSON(); + expect(tree).toMatchSnapshot(); + }); + it("with align", () => { + const tree = renderer + .create( + +
1
+
2
+
3
+
+ ) + .toJSON(); + expect(tree).toMatchSnapshot(); + }); + it("with wrap", () => { + const tree = renderer + .create( + +
1
+
2
+
3
+
+ ) + .toJSON(); + expect(tree).toMatchSnapshot(); + }); + }); +}); diff --git a/src/components/index.js b/src/components/index.js index 182b574f82..128149edfb 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -73,3 +73,4 @@ export { default as AccordionItem } from "./Accordion/AccordionItem/AccordionIte export { default as Clickable } from "./Clickable/Clickable"; export { default as ColorUtils } from "../utils/colors-utils"; export { default as IconButton } from "./IconButton/IconButton"; +export { default as Flex } from "./Flex/Flex"; diff --git a/src/constants/positions.js b/src/constants/positions.js new file mode 100644 index 0000000000..210b2dd4be --- /dev/null +++ b/src/constants/positions.js @@ -0,0 +1,5 @@ +export const BASE_POSITIONS = { + START: "Start", + CENTER: "Center", + END: "End" +}; diff --git a/src/constants/sizes.js b/src/constants/sizes.js index 4b1dd73d7f..92a5f3668e 100644 --- a/src/constants/sizes.js +++ b/src/constants/sizes.js @@ -1,9 +1,11 @@ +export const PASCAL_BASE_SIZE = Object.freeze({ SMALL: "Small", MEDIUM: "Medium", LARGE: "Large" }); + +export const BASE_SIZES = Object.freeze({ SMALL: "small", MEDIUM: "medium", LARGE: "large" }); + export const SIZES = Object.freeze({ XXS: "xxs", XS: "xs", - SMALL: "small", - MEDIUM: "medium", - LARGE: "large" + ...BASE_SIZES }); export const DialogPositions = Object.freeze({ diff --git a/src/storybook/components/related-components/component-description-map.js b/src/storybook/components/related-components/component-description-map.js index 24a227890d..d0bffb8a78 100644 --- a/src/storybook/components/related-components/component-description-map.js +++ b/src/storybook/components/related-components/component-description-map.js @@ -36,6 +36,7 @@ import { IconButtonDescription } from "./descriptions/icon-button-description"; import { MenuButtonDescription } from "./descriptions/menu-button-description"; import { ClickableDescription } from "./descriptions/clickable-description/clickable-description"; import { HiddenTextDescription } from "./descriptions/hidden-text-description"; +import { ListDescription } from "./descriptions/list"; export const SPLIT_BUTTON = "split-button"; export const BUTTON_GROUP = "button-group"; @@ -71,6 +72,7 @@ export const ICON_BUTTON = "icon-button"; export const MENU_BUTTON = "menu-button"; export const CLICKABLE = "clickable"; export const HIDDEN_TEXT = "hidden-text-description"; +export const LIST = "list"; // General description names (not related to specific components) export const COLORS = "colors"; @@ -114,6 +116,7 @@ descriptionTypesMap.set(EDITABLE_HEADING, ); descriptionTypesMap.set(HEADING, ); descriptionTypesMap.set(CLICKABLE, ); descriptionTypesMap.set(HIDDEN_TEXT, ); +descriptionTypesMap.set(LIST, ); // General description (not related to specific components) descriptionTypesMap.set(COLORS, ); diff --git a/src/storybook/components/related-components/descriptions/list.jsx b/src/storybook/components/related-components/descriptions/list.jsx new file mode 100644 index 0000000000..16bf01aeb0 --- /dev/null +++ b/src/storybook/components/related-components/descriptions/list.jsx @@ -0,0 +1,29 @@ +import { useMemo } from "react"; +import { RelatedComponent } from "../../related-component/related-component"; +import DialogContentContainer from "../../../../components/DialogContentContainer/DialogContentContainer"; +import ListItem from "../../../../components/ListItem/ListItem"; +import List from "../../../../components/List/List"; + +export const ListDescription = () => { + const component = useMemo(() => { + return ( +
+ + + List item 1 + List item 2 + List item 3 + + +
+ ); + }, []); + return ( + + ); +}; diff --git a/src/storybook/components/story-description/story-description.jsx b/src/storybook/components/story-description/story-description.jsx new file mode 100644 index 0000000000..bb76d07d96 --- /dev/null +++ b/src/storybook/components/story-description/story-description.jsx @@ -0,0 +1,34 @@ +import { useMemo } from "react"; +import cx from "classnames"; +import PropTypes from "prop-types"; +import Flex from "../../../components/Flex/Flex"; +import classes from "./story-description.module.scss"; + +export const StoryDescription = ({ description, children, vertical }) => { + const direction = useMemo(() => (vertical ? Flex.directions.COLUMN : Flex.directions.ROW), [vertical]); + return ( + + + {description} + + {children} + + ); +}; + +StoryDescription.propTypes = { + description: PropTypes.string, + children: PropTypes.element, + vertical: PropTypes.bool +}; + +StoryDescription.defaultProps = { + description: "", + children: null, + vertical: false +}; diff --git a/src/storybook/components/story-description/story-description.module.scss b/src/storybook/components/story-description/story-description.module.scss new file mode 100644 index 0000000000..f1646f2334 --- /dev/null +++ b/src/storybook/components/story-description/story-description.module.scss @@ -0,0 +1,7 @@ +.description { + font-weight: 500; + + &.vertical { + text-align: center; + } +} \ No newline at end of file