diff --git a/package-lock.json b/package-lock.json
index 5a35fae54dcf1e..acc6623a96913a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11948,10 +11948,10 @@
"@wordpress/primitives": "file:packages/primitives",
"@wordpress/rich-text": "file:packages/rich-text",
"@wordpress/warning": "file:packages/warning",
- "@wp-g2/components": "^0.0.140",
- "@wp-g2/context": "^0.0.140",
- "@wp-g2/styles": "^0.0.140",
- "@wp-g2/utils": "^0.0.140",
+ "@wp-g2/components": "^0.0.150",
+ "@wp-g2/context": "^0.0.150",
+ "@wp-g2/styles": "^0.0.150",
+ "@wp-g2/utils": "^0.0.150",
"classnames": "^2.2.5",
"dom-scroll-into-view": "^1.2.1",
"downshift": "^6.0.15",
@@ -11970,6 +11970,137 @@
"rememo": "^3.0.0",
"tinycolor2": "^1.4.2",
"uuid": "^8.3.0"
+ },
+ "dependencies": {
+ "@emotion/is-prop-valid": {
+ "version": "0.8.8",
+ "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz",
+ "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==",
+ "requires": {
+ "@emotion/memoize": "0.7.4"
+ }
+ },
+ "@emotion/memoize": {
+ "version": "0.7.4",
+ "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz",
+ "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw=="
+ },
+ "@wp-g2/components": {
+ "version": "0.0.150",
+ "resolved": "https://registry.npmjs.org/@wp-g2/components/-/components-0.0.150.tgz",
+ "integrity": "sha512-wolFRz5X53jFTgnqLfZlJue7xs3ucGHksR9zBsONKXwolDvING0cJLn9U17vuU7dB7WFzf8lLxKxPvdFyYMcoQ==",
+ "requires": {
+ "@popperjs/core": "^2.5.4",
+ "@wp-g2/context": "^0.0.150",
+ "@wp-g2/styles": "^0.0.150",
+ "@wp-g2/utils": "^0.0.150",
+ "csstype": "^3.0.3",
+ "downshift": "^6.0.15",
+ "framer-motion": "^2.1.0",
+ "highlight-words-core": "^1.2.2",
+ "history": "^4.9.0",
+ "lodash": "^4.17.19",
+ "path-to-regexp": "^1.7.0",
+ "react-colorful": "4.4.4",
+ "react-textarea-autosize": "^8.2.0",
+ "react-use-gesture": "^9.0.0",
+ "reakit": "^1.3.4"
+ }
+ },
+ "@wp-g2/context": {
+ "version": "0.0.150",
+ "resolved": "https://registry.npmjs.org/@wp-g2/context/-/context-0.0.150.tgz",
+ "integrity": "sha512-VaC79oGUhJ/4THEW0XupIhunovV0u5IMLZfIrDAs5k0eqp45CO30zghMv2DQBQnuSzI1PH6DJCoHGkn4DTBDfw==",
+ "requires": {
+ "@wp-g2/styles": "^0.0.150",
+ "@wp-g2/utils": "^0.0.150",
+ "lodash": "^4.17.19"
+ }
+ },
+ "@wp-g2/create-styles": {
+ "version": "0.0.150",
+ "resolved": "https://registry.npmjs.org/@wp-g2/create-styles/-/create-styles-0.0.150.tgz",
+ "integrity": "sha512-Dl2k/s9qtcJ86v2eTGfX/NdS9mjnCk1zWj0fEbCxKxdDCBEoGmYeFsBq16+ZEEA0YgyTzq8QmYjLhhSNGHwptQ==",
+ "requires": {
+ "@emotion/core": "^10.1.1",
+ "@emotion/is-prop-valid": "^0.8.8",
+ "@wp-g2/utils": "^0.0.150",
+ "create-emotion": "^10.0.27",
+ "emotion": "^10.0.27",
+ "emotion-theming": "^10.0.27",
+ "lodash": "^4.17.19",
+ "mitt": "^2.1.0",
+ "rtlcss": "^2.6.2",
+ "styled-griddie": "^0.1.3"
+ }
+ },
+ "@wp-g2/styles": {
+ "version": "0.0.150",
+ "resolved": "https://registry.npmjs.org/@wp-g2/styles/-/styles-0.0.150.tgz",
+ "integrity": "sha512-5CewOL0Ts5IdQIrW7d7Oa5joU1SWsVVBx9q+7rXfaXjFyeGliLrpRPchBk3OBPCQmw938kcexsa7eySlKFT67Q==",
+ "requires": {
+ "@wp-g2/create-styles": "^0.0.150",
+ "@wp-g2/utils": "^0.0.150"
+ }
+ },
+ "@wp-g2/utils": {
+ "version": "0.0.150",
+ "resolved": "https://registry.npmjs.org/@wp-g2/utils/-/utils-0.0.150.tgz",
+ "integrity": "sha512-CXJEZvwZwlLJfvsfWyxzAxd8qCVzZp9kK7w+GJ883K6NythPymOVJwECUEv2PG3xTUo5qwX4mAwePhA8ZPTHfQ==",
+ "requires": {
+ "copy-to-clipboard": "^3.3.1",
+ "create-emotion": "^10.0.27",
+ "deepmerge": "^4.2.2",
+ "fast-deep-equal": "^3.1.3",
+ "hoist-non-react-statics": "^3.3.2",
+ "json2mq": "^0.2.0",
+ "lodash": "^4.17.19",
+ "memize": "^1.1.0",
+ "react-merge-refs": "^1.1.0",
+ "react-resize-aware": "^3.1.0",
+ "tinycolor2": "^1.4.2",
+ "use-enhanced-state": "^0.0.13",
+ "use-isomorphic-layout-effect": "^1.0.0"
+ },
+ "dependencies": {
+ "react-merge-refs": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/react-merge-refs/-/react-merge-refs-1.1.0.tgz",
+ "integrity": "sha512-alTKsjEL0dKH/ru1Iyn7vliS2QRcBp9zZPGoWxUOvRGWPUYgjo+V01is7p04It6KhgrzhJGnIj9GgX8W4bZoCQ=="
+ }
+ }
+ },
+ "csstype": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.6.tgz",
+ "integrity": "sha512-+ZAmfyWMT7TiIlzdqJgjMb7S4f1beorDbWbsocyK4RaiqA5RTX3K14bnBWmmA9QEM0gRdsjyyrEmcyga8Zsxmw=="
+ },
+ "fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
+ },
+ "hoist-non-react-statics": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
+ "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
+ "requires": {
+ "react-is": "^16.7.0"
+ }
+ },
+ "isarray": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+ "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
+ },
+ "path-to-regexp": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
+ "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
+ "requires": {
+ "isarray": "0.0.1"
+ }
+ }
}
},
"@wordpress/compose": {
@@ -12856,186 +12987,6 @@
"lodash": "^4.17.19"
}
},
- "@wp-g2/components": {
- "version": "0.0.140",
- "resolved": "https://registry.npmjs.org/@wp-g2/components/-/components-0.0.140.tgz",
- "integrity": "sha512-bychuhZ3wPSB457CHYcogoPQPlP/eUA9GoTo0Fv0rj7f44Gr9XlPoqVT+GQa3CmPnvSCAl1sjoe75Vkaoo/O1w==",
- "requires": {
- "@popperjs/core": "^2.5.4",
- "@wp-g2/context": "^0.0.140",
- "@wp-g2/styles": "^0.0.140",
- "@wp-g2/utils": "^0.0.140",
- "csstype": "^3.0.3",
- "downshift": "^6.0.15",
- "framer-motion": "^2.1.0",
- "highlight-words-core": "^1.2.2",
- "history": "^4.9.0",
- "lodash": "^4.17.19",
- "path-to-regexp": "^1.7.0",
- "react-colorful": "4.4.4",
- "react-textarea-autosize": "^8.2.0",
- "react-use-gesture": "^9.0.0",
- "reakit": "1.1.0"
- },
- "dependencies": {
- "csstype": {
- "version": "3.0.6",
- "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.6.tgz",
- "integrity": "sha512-+ZAmfyWMT7TiIlzdqJgjMb7S4f1beorDbWbsocyK4RaiqA5RTX3K14bnBWmmA9QEM0gRdsjyyrEmcyga8Zsxmw=="
- },
- "isarray": {
- "version": "0.0.1",
- "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
- "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
- },
- "path-to-regexp": {
- "version": "1.8.0",
- "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
- "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
- "requires": {
- "isarray": "0.0.1"
- }
- },
- "reakit": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/reakit/-/reakit-1.1.0.tgz",
- "integrity": "sha512-d/ERtwgBndBPsyPBPUl5jueyfFgsglIfQCnLMKuxM0PaWiIZ6Ys3XsYaNy/AaG8k46Ee5cQPMdRrR30nVcSToQ==",
- "requires": {
- "@popperjs/core": "^2.4.2",
- "body-scroll-lock": "^3.0.2",
- "reakit-system": "^0.13.0",
- "reakit-utils": "^0.13.0",
- "reakit-warning": "^0.4.0"
- }
- },
- "reakit-system": {
- "version": "0.13.1",
- "resolved": "https://registry.npmjs.org/reakit-system/-/reakit-system-0.13.1.tgz",
- "integrity": "sha512-qglfQ53FsJh5+VSkjMtBg7eZiowj9zXOyfJJxfaXh/XYTVe/5ibzWg6rvGHyvSm6C3D7Q2sg/NPCLmCtYGGvQA==",
- "requires": {
- "reakit-utils": "^0.13.1"
- }
- },
- "reakit-utils": {
- "version": "0.13.1",
- "resolved": "https://registry.npmjs.org/reakit-utils/-/reakit-utils-0.13.1.tgz",
- "integrity": "sha512-NBKgsot3tU91gZgK5MTInI/PR0T3kIsTmbU5MbGggSOcwU2dG/kbE8IrM2lC6ayCSL2W2QWkijT6kewdrIX7Gw=="
- },
- "reakit-warning": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/reakit-warning/-/reakit-warning-0.4.1.tgz",
- "integrity": "sha512-AgnRN6cf8DYBF/mK2JEMFVL67Sbon8fDbFy1kfm0EDibtGsMOQtsFYfozZL7TwmJ4yg68VMhg8tmPHchVQRrlg==",
- "requires": {
- "reakit-utils": "^0.13.1"
- }
- }
- }
- },
- "@wp-g2/context": {
- "version": "0.0.140",
- "resolved": "https://registry.npmjs.org/@wp-g2/context/-/context-0.0.140.tgz",
- "integrity": "sha512-z32fxZ2tCVmYQC+wyyziyrhEvWBPFBQfUhUHF85JmTUPzQQeEPiLC3rgDAT0fUTFlJHinPJQq6871RDqFSwCUA==",
- "requires": {
- "@wp-g2/styles": "^0.0.140",
- "@wp-g2/utils": "^0.0.140",
- "lodash": "^4.17.19"
- }
- },
- "@wp-g2/create-styles": {
- "version": "0.0.140",
- "resolved": "https://registry.npmjs.org/@wp-g2/create-styles/-/create-styles-0.0.140.tgz",
- "integrity": "sha512-/60DxWjCAhsoYOqY7aiHVbkTAF+L6qZIyHyH50oNs9FTVkcRLHQFSC0kHgAam+Z9K3eImQ7hM52wfBDqae0q2Q==",
- "requires": {
- "@emotion/core": "^10.1.1",
- "@emotion/is-prop-valid": "^0.8.8",
- "@wp-g2/utils": "^0.0.140",
- "create-emotion": "^10.0.27",
- "emotion": "^10.0.27",
- "emotion-theming": "^10.0.27",
- "lodash": "^4.17.19",
- "mitt": "^2.1.0",
- "rtlcss": "^2.6.2",
- "styled-griddie": "^0.1.3"
- },
- "dependencies": {
- "@emotion/is-prop-valid": {
- "version": "0.8.8",
- "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz",
- "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==",
- "requires": {
- "@emotion/memoize": "0.7.4"
- }
- },
- "@emotion/memoize": {
- "version": "0.7.4",
- "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz",
- "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw=="
- }
- }
- },
- "@wp-g2/styles": {
- "version": "0.0.140",
- "resolved": "https://registry.npmjs.org/@wp-g2/styles/-/styles-0.0.140.tgz",
- "integrity": "sha512-wAvtqQOqX2zYpfEdVK4l4abH/hUUgw/+8+E5PvPgrsvqFg8IehNSksnjNF5/IloLRGAH70d8ytjMuMnUK8PVYA==",
- "requires": {
- "@wp-g2/create-styles": "^0.0.140",
- "@wp-g2/utils": "^0.0.140"
- }
- },
- "@wp-g2/utils": {
- "version": "0.0.140",
- "resolved": "https://registry.npmjs.org/@wp-g2/utils/-/utils-0.0.140.tgz",
- "integrity": "sha512-a4uYi/XQEDrOAIO3JUQ+L/oeSkgp+08pSy41xxQ1nIRHs7X+Du84X2EFQrvZfGBRuXuVlVuUIlN2e0IE8yUZKw==",
- "requires": {
- "copy-to-clipboard": "^3.3.1",
- "create-emotion": "^10.0.27",
- "deepmerge": "^4.2.2",
- "fast-deep-equal": "^3.1.3",
- "hoist-non-react-statics": "^3.3.2",
- "json2mq": "^0.2.0",
- "lodash": "^4.17.19",
- "memize": "^1.1.0",
- "react-merge-refs": "^1.1.0",
- "react-resize-aware": "^3.1.0",
- "reakit-warning": "^0.5.5",
- "tinycolor2": "^1.4.2",
- "use-enhanced-state": "^0.0.13",
- "use-isomorphic-layout-effect": "^1.0.0"
- },
- "dependencies": {
- "fast-deep-equal": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
- "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
- },
- "hoist-non-react-statics": {
- "version": "3.3.2",
- "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
- "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
- "requires": {
- "react-is": "^16.7.0"
- }
- },
- "react-merge-refs": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/react-merge-refs/-/react-merge-refs-1.1.0.tgz",
- "integrity": "sha512-alTKsjEL0dKH/ru1Iyn7vliS2QRcBp9zZPGoWxUOvRGWPUYgjo+V01is7p04It6KhgrzhJGnIj9GgX8W4bZoCQ=="
- },
- "reakit-utils": {
- "version": "0.14.4",
- "resolved": "https://registry.npmjs.org/reakit-utils/-/reakit-utils-0.14.4.tgz",
- "integrity": "sha512-jDEf/NmZVJ6fs10G16ifD+RFhQikSLN7VfjRHu0CPoUj4g6lFXd5PPcRXCY81qiqc9FVHjr2d2fmsw1hs6xUxA=="
- },
- "reakit-warning": {
- "version": "0.5.5",
- "resolved": "https://registry.npmjs.org/reakit-warning/-/reakit-warning-0.5.5.tgz",
- "integrity": "sha512-OuP1r7rlSSJZsoLuc0CPA2ACPKnWO8HDbFktiiidbT67UjuX6udYV1AUsIgMJ8ado9K5gZGjPj7IB/GDYo9Yjg==",
- "requires": {
- "reakit-utils": "^0.14.4"
- }
- }
- }
- },
"@xtuc/ieee754": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
diff --git a/packages/components/package.json b/packages/components/package.json
index 89c40661ab3329..79b0df3c4a3320 100644
--- a/packages/components/package.json
+++ b/packages/components/package.json
@@ -45,10 +45,10 @@
"@wordpress/primitives": "file:../primitives",
"@wordpress/rich-text": "file:../rich-text",
"@wordpress/warning": "file:../warning",
- "@wp-g2/components": "^0.0.140",
- "@wp-g2/context": "^0.0.140",
- "@wp-g2/styles": "^0.0.140",
- "@wp-g2/utils": "^0.0.140",
+ "@wp-g2/components": "^0.0.150",
+ "@wp-g2/context": "^0.0.150",
+ "@wp-g2/styles": "^0.0.150",
+ "@wp-g2/utils": "^0.0.150",
"classnames": "^2.2.5",
"dom-scroll-into-view": "^1.2.1",
"downshift": "^6.0.15",
diff --git a/packages/components/src/ui/flex/types.ts b/packages/components/src/ui/flex/types.ts
index 3e3c84fa4fdf32..36036885727f5c 100644
--- a/packages/components/src/ui/flex/types.ts
+++ b/packages/components/src/ui/flex/types.ts
@@ -109,7 +109,7 @@ export type FlexProps = {
* }
* ```
*/
- gap?: number;
+ gap?: import('react').ReactText;
/**
* Horizontally aligns content if the `direction` is `row`, or vertically aligns content if the `direction` is `column`.
* In the example below, `flex-start` will align the children content to the left.
diff --git a/packages/components/src/ui/h-stack/README.md b/packages/components/src/ui/h-stack/README.md
new file mode 100644
index 00000000000000..bf627f85c39937
--- /dev/null
+++ b/packages/components/src/ui/h-stack/README.md
@@ -0,0 +1,127 @@
+# HStack
+
+`HStack` (Horizontal Stack) arranges child elements in a horizontal line.
+
+## Usage
+
+`HStack` can render anything inside.
+
+```jsx
+function Example() {
+ return (
+
+
+ Ana
+
+
+ Elsa
+
+
+ Olaf
+
+
+ );
+}
+```
+
+## Props
+
+##### alignment
+
+**Type**: `HStackAlignment` | `CSS[ 'alignItems' ]`
+
+Determines how the child elements are aligned.
+
+- `top`: Aligns content to the top.
+- `topLeft`: Aligns content to the top/left.
+- `topRight`: Aligns content to the top/right.
+- `left`: Aligns content to the left.
+- `center`: Aligns content to the center.
+- `right`: Aligns content to the right.
+- `bottom`: Aligns content to the bottom.
+- `bottomLeft`: Aligns content to the bottom/left.
+- `bottomRight`: Aligns content to the bottom/right.
+- `edge`: Aligns content to the edges of the container.
+- `stretch`: Stretches content to the edges of the container.
+
+##### direction
+
+**Type**: `FlexDirection`
+
+The direction flow of the children content can be adjusted with `direction`. `column` will align children vertically and `row` will align children horizontally.
+
+##### expanded
+
+**Type**: `boolean`
+
+Expands to the maximum available width (if horizontal) or height (if vertical).
+
+##### justify
+
+**Type**: `CSS['justifyContent']`
+
+Horizontally aligns content if the `direction` is `row`, or vertically aligns content if the `direction` is `column`.
+In the example below, `flex-start` will align the children content to the left.
+
+##### spacing
+
+**Type**: `CSS['width']`
+
+The amount of space between each child element. Spacing in between each child can be adjusted by using `spacing`.
+The value of `spacing` works as a multiplier to the library's grid system (base of `4px`).
+
+##### wrap
+
+**Type**: `boolean`
+
+Determines if children should wrap.
+
+## Spacer
+
+When a `Spacer` is used within an `HStack`, the `Spacer` adaptively expands to take up the remaining space.
+
+```jsx live
+import { HStack, Spacer, Text, View } from '@wp-g2/components';
+import { ui } from '@wp-g2/styles';
+
+function Example() {
+ return (
+
+
+ Ana
+
+
+ Elsa
+
+
+ Olaf
+
+
+ );
+}
+```
+
+`Spacer` also be used in-between items to push them apart.
+
+```jsx live
+import { HStack, Spacer, Text, View } from '@wp-g2/components';
+import { ui } from '@wp-g2/styles';
+
+function Example() {
+ return (
+
+
+ Ana
+
+
+
+ Elsa
+
+
+ Olaf
+
+
+ );
+}
+```
+
diff --git a/packages/components/src/ui/h-stack/h-stack-utils.js b/packages/components/src/ui/h-stack/h-stack-utils.js
new file mode 100644
index 00000000000000..288cdc79b7db96
--- /dev/null
+++ b/packages/components/src/ui/h-stack/h-stack-utils.js
@@ -0,0 +1,56 @@
+/**
+ * External dependencies
+ */
+import { isNil } from 'lodash';
+
+/** @type {import('./types').Alignments} */
+const ALIGNMENTS = {
+ bottom: { align: 'flex-end', justify: 'center' },
+ bottomLeft: { align: 'flex-start', justify: 'flex-end' },
+ bottomRight: { align: 'flex-end', justify: 'flex-end' },
+ center: { align: 'center', justify: 'center' },
+ edge: { align: 'center', justify: 'space-between' },
+ left: { align: 'center', justify: 'flex-start' },
+ right: { align: 'center', justify: 'flex-end' },
+ stretch: { align: 'stretch' },
+ top: { align: 'flex-start', justify: 'center' },
+ topLeft: { align: 'flex-start', justify: 'flex-start' },
+ topRight: { align: 'flex-start', justify: 'flex-end' },
+};
+
+/** @type {import('./types').Alignments} */
+const V_ALIGNMENTS = {
+ bottom: { justify: 'flex-end', align: 'center' },
+ bottomLeft: { justify: 'flex-start', align: 'flex-end' },
+ bottomRight: { justify: 'flex-end', align: 'flex-end' },
+ center: { justify: 'center', align: 'center' },
+ edge: { justify: 'space-between', align: 'center' },
+ left: { justify: 'center', align: 'flex-start' },
+ right: { justify: 'center', align: 'flex-end' },
+ stretch: { justify: 'stretch' },
+ top: { justify: 'flex-start', align: 'center' },
+ topLeft: { justify: 'flex-start', align: 'flex-start' },
+ topRight: { justify: 'flex-start', align: 'flex-end' },
+};
+
+/* eslint-disable jsdoc/valid-types */
+/**
+ * @param {import('./types').HStackAlignment | import('react').CSSProperties[ 'alignItems' ]} alignment Where to align.
+ * @param {import('../flex/types').FlexDirection} [direction='row'] Direction to align.
+ * @return {import('./types').AlignmentProps} Alignment props.
+ */
+/* eslint-enable jsdoc/valid-types */
+export function getAlignmentProps( alignment, direction = 'row' ) {
+ if ( isNil( alignment ) ) {
+ return {};
+ }
+ const isVertical = direction === 'column';
+ const props = isVertical ? V_ALIGNMENTS : ALIGNMENTS;
+
+ const alignmentProps =
+ alignment in props
+ ? props[ /** @type {keyof typeof ALIGNMENTS} */ ( alignment ) ]
+ : { align: alignment };
+
+ return alignmentProps;
+}
diff --git a/packages/components/src/ui/h-stack/h-stack.js b/packages/components/src/ui/h-stack/h-stack.js
new file mode 100644
index 00000000000000..8ffdda1f28fe85
--- /dev/null
+++ b/packages/components/src/ui/h-stack/h-stack.js
@@ -0,0 +1,11 @@
+/**
+ * Internal dependencies
+ */
+import { createComponent } from '../utils';
+import { useHStack } from './use-h-stack';
+
+export default createComponent( {
+ as: 'div',
+ useHook: useHStack,
+ name: 'HStack',
+} );
diff --git a/packages/components/src/ui/h-stack/index.js b/packages/components/src/ui/h-stack/index.js
new file mode 100644
index 00000000000000..b52a8c22b4d2c7
--- /dev/null
+++ b/packages/components/src/ui/h-stack/index.js
@@ -0,0 +1,2 @@
+export { default as HStack } from './h-stack';
+export * from './use-h-stack';
diff --git a/packages/components/src/ui/h-stack/stories/HStack.stories.js b/packages/components/src/ui/h-stack/stories/HStack.stories.js
new file mode 100644
index 00000000000000..1d777bc6ea7a61
--- /dev/null
+++ b/packages/components/src/ui/h-stack/stories/HStack.stories.js
@@ -0,0 +1,22 @@
+/**
+ * Internal dependencies
+ */
+import { View } from '../../view';
+import { HStack } from '../index';
+
+export default {
+ component: HStack,
+ title: 'G2 Components (Experimental)/HStack',
+};
+
+export const _default = () => {
+ return (
+
+ One
+ Two
+ Three
+ Four
+ Five
+
+ );
+};
diff --git a/packages/components/src/ui/h-stack/test/__snapshots__/index.js.snap b/packages/components/src/ui/h-stack/test/__snapshots__/index.js.snap
new file mode 100644
index 00000000000000..2238493a1747f0
--- /dev/null
+++ b/packages/components/src/ui/h-stack/test/__snapshots__/index.js.snap
@@ -0,0 +1,295 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`props should render alignment 1`] = `
+.emotion-0.emotion-0.emotion-0.emotion-0.emotion-0.emotion-0.emotion-0 {
+ box-sizing: border-box;
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-font-smoothing: antialiased;
+ font-family: Inter,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;
+ font-family: var(--wp-g2-font-family);
+ font-size: 13px;
+ font-size: var(--wp-g2-font-size);
+ font-weight: normal;
+ font-weight: var(--wp-g2-font-weight);
+ margin: 0;
+}
+
+@media (prefers-reduced-motion) {
+ .emotion-0.emotion-0.emotion-0.emotion-0.emotion-0.emotion-0.emotion-0 {
+ -webkit-transition: none !important;
+ transition: none !important;
+ }
+}
+
+[data-system-ui-reduced-motion-mode="true"] .emotion-0.emotion-0.emotion-0.emotion-0.emotion-0.emotion-0.emotion-0 {
+ -webkit-transition: none !important;
+ transition: none !important;
+}
+
+.emotion-2.emotion-2.emotion-2.emotion-2.emotion-2.emotion-2.emotion-2 {
+ box-sizing: border-box;
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-font-smoothing: antialiased;
+ font-family: Inter,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;
+ font-family: var(--wp-g2-font-family);
+ font-size: 13px;
+ font-size: var(--wp-g2-font-size);
+ font-weight: normal;
+ font-weight: var(--wp-g2-font-weight);
+ margin: 0;
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ --wp-g2-flex-gap: calc(4px * 2);
+ --wp-g2-flex-gap: calc(var(--wp-g2-grid-base) * 2);
+ --wp-g2-flex-item-margin-bottom: 0;
+ --wp-g2-flex-item-margin-right: calc(4px * 2);
+ --wp-g2-flex-item-margin-right: var(--wp-g2-flex-gap);
+ --wp-g2-flex-item-margin-left: 0;
+ -webkit-align-items: center;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ -webkit-flex-direction: row;
+ -ms-flex-direction: row;
+ flex-direction: row;
+ -webkit-box-pack: center;
+ -webkit-justify-content: center;
+ -ms-flex-pack: center;
+ justify-content: center;
+ width: 100%;
+ --wp-g2-h-stack-spacing: calc(4px * 2);
+ --wp-g2-h-stack-spacing: calc(var(--wp-g2-grid-base) * 2);
+}
+
+@media (prefers-reduced-motion) {
+ .emotion-2.emotion-2.emotion-2.emotion-2.emotion-2.emotion-2.emotion-2 {
+ -webkit-transition: none !important;
+ transition: none !important;
+ }
+}
+
+[data-system-ui-reduced-motion-mode="true"] .emotion-2.emotion-2.emotion-2.emotion-2.emotion-2.emotion-2.emotion-2 {
+ -webkit-transition: none !important;
+ transition: none !important;
+}
+
+.emotion-2.emotion-2.emotion-2.emotion-2.emotion-2.emotion-2.emotion-2 > * + *:not(marquee) {
+ margin-left: calc(4px * 2);
+ margin-left: calc(var(--wp-g2-grid-base) * 2);
+}
+
+.emotion-2.emotion-2.emotion-2.emotion-2.emotion-2.emotion-2.emotion-2 > * {
+ min-width: 0;
+}
+
+
+`;
+
+exports[`props should render correctly 1`] = `
+.emotion-2.emotion-2.emotion-2.emotion-2.emotion-2.emotion-2.emotion-2 {
+ box-sizing: border-box;
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-font-smoothing: antialiased;
+ font-family: Inter,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;
+ font-family: var(--wp-g2-font-family);
+ font-size: 13px;
+ font-size: var(--wp-g2-font-size);
+ font-weight: normal;
+ font-weight: var(--wp-g2-font-weight);
+ margin: 0;
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ --wp-g2-flex-gap: calc(4px * 2);
+ --wp-g2-flex-gap: calc(var(--wp-g2-grid-base) * 2);
+ --wp-g2-flex-item-margin-bottom: 0;
+ --wp-g2-flex-item-margin-right: calc(4px * 2);
+ --wp-g2-flex-item-margin-right: var(--wp-g2-flex-gap);
+ --wp-g2-flex-item-margin-left: 0;
+ -webkit-align-items: center;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ -webkit-flex-direction: row;
+ -ms-flex-direction: row;
+ flex-direction: row;
+ -webkit-box-pack: justify;
+ -webkit-justify-content: space-between;
+ -ms-flex-pack: justify;
+ justify-content: space-between;
+ width: 100%;
+ --wp-g2-h-stack-spacing: calc(4px * 2);
+ --wp-g2-h-stack-spacing: calc(var(--wp-g2-grid-base) * 2);
+}
+
+@media (prefers-reduced-motion) {
+ .emotion-2.emotion-2.emotion-2.emotion-2.emotion-2.emotion-2.emotion-2 {
+ -webkit-transition: none !important;
+ transition: none !important;
+ }
+}
+
+[data-system-ui-reduced-motion-mode="true"] .emotion-2.emotion-2.emotion-2.emotion-2.emotion-2.emotion-2.emotion-2 {
+ -webkit-transition: none !important;
+ transition: none !important;
+}
+
+.emotion-2.emotion-2.emotion-2.emotion-2.emotion-2.emotion-2.emotion-2 > * + *:not(marquee) {
+ margin-left: calc(4px * 2);
+ margin-left: calc(var(--wp-g2-grid-base) * 2);
+}
+
+.emotion-2.emotion-2.emotion-2.emotion-2.emotion-2.emotion-2.emotion-2 > * {
+ min-width: 0;
+}
+
+.emotion-0.emotion-0.emotion-0.emotion-0.emotion-0.emotion-0.emotion-0 {
+ box-sizing: border-box;
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-font-smoothing: antialiased;
+ font-family: Inter,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;
+ font-family: var(--wp-g2-font-family);
+ font-size: 13px;
+ font-size: var(--wp-g2-font-size);
+ font-weight: normal;
+ font-weight: var(--wp-g2-font-weight);
+ margin: 0;
+}
+
+@media (prefers-reduced-motion) {
+ .emotion-0.emotion-0.emotion-0.emotion-0.emotion-0.emotion-0.emotion-0 {
+ -webkit-transition: none !important;
+ transition: none !important;
+ }
+}
+
+[data-system-ui-reduced-motion-mode="true"] .emotion-0.emotion-0.emotion-0.emotion-0.emotion-0.emotion-0.emotion-0 {
+ -webkit-transition: none !important;
+ transition: none !important;
+}
+
+
+`;
+
+exports[`props should render spacing 1`] = `
+.emotion-0.emotion-0.emotion-0.emotion-0.emotion-0.emotion-0.emotion-0 {
+ box-sizing: border-box;
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-font-smoothing: antialiased;
+ font-family: Inter,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;
+ font-family: var(--wp-g2-font-family);
+ font-size: 13px;
+ font-size: var(--wp-g2-font-size);
+ font-weight: normal;
+ font-weight: var(--wp-g2-font-weight);
+ margin: 0;
+}
+
+@media (prefers-reduced-motion) {
+ .emotion-0.emotion-0.emotion-0.emotion-0.emotion-0.emotion-0.emotion-0 {
+ -webkit-transition: none !important;
+ transition: none !important;
+ }
+}
+
+[data-system-ui-reduced-motion-mode="true"] .emotion-0.emotion-0.emotion-0.emotion-0.emotion-0.emotion-0.emotion-0 {
+ -webkit-transition: none !important;
+ transition: none !important;
+}
+
+.emotion-2.emotion-2.emotion-2.emotion-2.emotion-2.emotion-2.emotion-2 {
+ box-sizing: border-box;
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-font-smoothing: antialiased;
+ font-family: Inter,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;
+ font-family: var(--wp-g2-font-family);
+ font-size: 13px;
+ font-size: var(--wp-g2-font-size);
+ font-weight: normal;
+ font-weight: var(--wp-g2-font-weight);
+ margin: 0;
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ --wp-g2-flex-gap: calc(4px * 5);
+ --wp-g2-flex-gap: calc(var(--wp-g2-grid-base) * 5);
+ --wp-g2-flex-item-margin-bottom: 0;
+ --wp-g2-flex-item-margin-right: calc(4px * 2);
+ --wp-g2-flex-item-margin-right: var(--wp-g2-flex-gap);
+ --wp-g2-flex-item-margin-left: 0;
+ -webkit-align-items: center;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ -webkit-flex-direction: row;
+ -ms-flex-direction: row;
+ flex-direction: row;
+ -webkit-box-pack: justify;
+ -webkit-justify-content: space-between;
+ -ms-flex-pack: justify;
+ justify-content: space-between;
+ width: 100%;
+ --wp-g2-h-stack-spacing: calc(4px * 5);
+ --wp-g2-h-stack-spacing: calc(var(--wp-g2-grid-base) * 5);
+}
+
+@media (prefers-reduced-motion) {
+ .emotion-2.emotion-2.emotion-2.emotion-2.emotion-2.emotion-2.emotion-2 {
+ -webkit-transition: none !important;
+ transition: none !important;
+ }
+}
+
+[data-system-ui-reduced-motion-mode="true"] .emotion-2.emotion-2.emotion-2.emotion-2.emotion-2.emotion-2.emotion-2 {
+ -webkit-transition: none !important;
+ transition: none !important;
+}
+
+.emotion-2.emotion-2.emotion-2.emotion-2.emotion-2.emotion-2.emotion-2 > * + *:not(marquee) {
+ margin-left: calc(4px * 5);
+ margin-left: calc(var(--wp-g2-grid-base) * 5);
+}
+
+.emotion-2.emotion-2.emotion-2.emotion-2.emotion-2.emotion-2.emotion-2 > * {
+ min-width: 0;
+}
+
+
+`;
diff --git a/packages/components/src/ui/h-stack/test/index.js b/packages/components/src/ui/h-stack/test/index.js
new file mode 100644
index 00000000000000..56190f81db4cdd
--- /dev/null
+++ b/packages/components/src/ui/h-stack/test/index.js
@@ -0,0 +1,42 @@
+/**
+ * External dependencies
+ */
+import { render } from '@testing-library/react';
+
+/**
+ * Internal dependencies
+ */
+import { View } from '../../view';
+import { HStack } from '..';
+
+describe( 'props', () => {
+ test( 'should render correctly', () => {
+ const { container } = render(
+
+
+
+
+ );
+ expect( container.firstChild ).toMatchSnapshot();
+ } );
+
+ test( 'should render alignment', () => {
+ const { container } = render(
+
+
+
+
+ );
+ expect( container.firstChild ).toMatchSnapshot();
+ } );
+
+ test( 'should render spacing', () => {
+ const { container } = render(
+
+
+
+
+ );
+ expect( container.firstChild ).toMatchSnapshot();
+ } );
+} );
diff --git a/packages/components/src/ui/h-stack/types.ts b/packages/components/src/ui/h-stack/types.ts
new file mode 100644
index 00000000000000..694b3866af5add
--- /dev/null
+++ b/packages/components/src/ui/h-stack/types.ts
@@ -0,0 +1,123 @@
+import { CSSProperties } from 'react';
+import { FlexProps } from '../flex/types';
+
+export type HStackAlignment =
+ | 'bottom'
+ | 'bottomLeft'
+ | 'bottomRight'
+ | 'center'
+ | 'edge'
+ | 'left'
+ | 'right'
+ | 'stretch'
+ | 'top'
+ | 'topLeft'
+ | 'topRight';
+
+export type AlignmentProps = {
+ justify?: CSSProperties[ 'justifyContent' ];
+ align?: CSSProperties[ 'alignItems' ];
+};
+
+export type Alignments = Record;
+
+export type Props = Omit< FlexProps, 'align' | 'gap' > & {
+ /**
+ * Determines how the child elements are aligned.
+ *
+ * * `top`: Aligns content to the top.
+ * * `topLeft`: Aligns content to the top/left.
+ * * `topRight`: Aligns content to the top/right.
+ * * `left`: Aligns content to the left.
+ * * `center`: Aligns content to the center.
+ * * `right`: Aligns content to the right.
+ * * `bottom`: Aligns content to the bottom.
+ * * `bottomLeft`: Aligns content to the bottom/left.
+ * * `bottomRight`: Aligns content to the bottom/right.
+ * * `edge`: Aligns content to the edges of the container.
+ * * `stretch`: Stretches content to the edges of the container.
+ *
+ * @default 'edge'
+ *
+ * @example
+ *```jsx
+ * import { HStack, Text, View } from `@wp-g2/components`
+ * import { ui } from `@wp-g2/styles`
+ *
+ * function Example() {
+ * return (
+ *
+ *
+ * Ana
+ *
+ *
+ * Elsa
+ *
+ *
+ * Olaf
+ *
+ *
+ * );
+ * }
+ *```
+ */
+ alignment?: HStackAlignment | CSSProperties[ 'alignItems' ];
+ /**
+ * The amount of space between each child element. Spacing in between each child can be adjusted by using `spacing`.
+ * The value of `spacing` works as a multiplier to the library's grid system (base of `4px`).
+ *
+ * @default 2
+ *
+ * @example
+ * ```jsx
+ * import { HStack, Text, View } from `@wp-g2/components`
+ * import { ui } from `@wp-g2/styles`
+ *
+ * function Example() {
+ * return (
+ *
+ *
+ * Ana
+ *
+ *
+ * Elsa
+ *
+ *
+ * Olaf
+ *
+ *
+ * );
+ * }
+ *```
+ */
+ spacing?: CSSProperties[ 'width' ];
+};
+
+/**
+ * `HStack` (Horizontal Stack) arranges child elements in a horizontal line.
+ *
+ * @remarks
+ * `HStack` can render anything inside.
+ *
+ * @example
+ * ```jsx
+ * import { HStack, Text, View } from `@wp-g2/components`
+ * import { ui } from `@wp-g2/styles`
+ *
+ * function Example() {
+ * return (
+ *
+ *
+ * Ana
+ *
+ *
+ * Elsa
+ *
+ *
+ * Olaf
+ *
+ *
+ * );
+ * }
+ * ```
+ */
diff --git a/packages/components/src/ui/h-stack/use-h-stack.js b/packages/components/src/ui/h-stack/use-h-stack.js
new file mode 100644
index 00000000000000..1d79efa22d89d4
--- /dev/null
+++ b/packages/components/src/ui/h-stack/use-h-stack.js
@@ -0,0 +1,79 @@
+/**
+ * External dependencies
+ */
+import { hasNamespace, useContextSystem } from '@wp-g2/context';
+import { css, cx, ui } from '@wp-g2/styles';
+import { getValidChildren } from '@wp-g2/utils';
+
+/**
+ * WordPress dependencies
+ */
+import { useMemo } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import { FlexItem, useFlex } from '../flex';
+import { getAlignmentProps } from './h-stack-utils';
+
+/**
+ *
+ * @param {import('@wp-g2/create-styles').ViewOwnProps} props
+ */
+export function useHStack( props ) {
+ const {
+ alignment = 'edge',
+ children,
+ className,
+ direction,
+ spacing = 2,
+ ...otherProps
+ } = useContextSystem( props, 'HStack' );
+
+ const align = getAlignmentProps( alignment, direction );
+
+ const validChildren = getValidChildren( children );
+ const clonedChildren = validChildren.map(
+ // @ts-ignore
+ ( /** @type {import('react').ReactElement} */ child, index ) => {
+ const _key = child.key || `hstack-${ index }`;
+ const _isSpacer = hasNamespace( child, [ 'Spacer' ] );
+
+ if ( _isSpacer ) {
+ return (
+
+ );
+ }
+
+ return child;
+ }
+ );
+
+ const classes = useMemo( () => {
+ return cx(
+ css( {
+ [ ui.createToken( 'HStackSpacing' ) ]: ui.space( spacing ),
+ } ),
+ className
+ );
+ }, [ className, spacing ] );
+
+ const propsForFlex = {
+ className: classes,
+ children: clonedChildren,
+ direction,
+ justify: 'center',
+ ...align,
+ ...otherProps,
+ gap: spacing,
+ };
+
+ const flexProps = useFlex( propsForFlex );
+
+ return flexProps;
+}
diff --git a/packages/e2e-tests/specs/editor/various/font-size-picker.test.js b/packages/e2e-tests/specs/editor/various/font-size-picker.test.js
index 84edbd564b5352..4b92a9fb427f22 100644
--- a/packages/e2e-tests/specs/editor/various/font-size-picker.test.js
+++ b/packages/e2e-tests/specs/editor/various/font-size-picker.test.js
@@ -86,8 +86,14 @@ describe( 'Font Size Picker', () => {
);
await first( await page.$x( FONT_SIZE_LABEL_SELECTOR ) ).click();
+
+ // Disable reason: Wait for changes to apply.
+ // eslint-disable-next-line no-restricted-syntax
+ await page.waitForTimeout( 100 );
+
await pressKeyTimes( 'ArrowDown', 2 );
await page.keyboard.press( 'Enter' );
+
await page.keyboard.press( 'Tab' );
await page.keyboard.press( 'Tab' );
@@ -114,6 +120,10 @@ describe( 'Font Size Picker', () => {
await pressKeyTimes( 'Backspace', 5 );
await page.keyboard.press( 'Enter' );
+ // Disable reason: Wait for changes to apply.
+ // eslint-disable-next-line no-restricted-syntax
+ await page.waitForTimeout( 1000 );
+
// Ensure content matches snapshot.
const content = await getEditedPostContent();
expect( content ).toMatchSnapshot();