Repository: marigold-ui/marigold
Pull request #2696 (2022-12-16 15:05:03)

feat: add FieldBaseGroup component
- build for implementing the typical Core Forms (label on the left etc.)
- with that you can use labelWidth as prop to align the pieces arrange in order

changeset-bot[bot]: ###  ⚠️  No Changeset found

Latest commit: ee6ca6e64ecc2ef18256f4f20453ac925a93423f

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. **If these changes should result in a version bump, you need to add a changeset.**

<details><summary>This PR includes no changesets</summary>

  When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

</details>

[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/master/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/marigold-ui/marigold/new/fieldbase-group?filename=.changeset/green-squids-raise.md&value=---%0A%22%40marigold%2Fdocs%22%3A%20patch%0A%22%40marigold%2Fcomponents%22%3A%20patch%0A%22%40marigold%2Ftheme-b2b%22%3A%20patch%0A%22%40marigold%2Ftheme-core%22%3A%20patch%0A%22%40marigold%2Ftheme-unicorn%22%3A%20patch%0A---%0A%0Afeat%3A%20add%20FieldBaseGroup%20component%0A)



Patch:
diff --git a/.changeset/few-tigers-sell.md b/.changeset/few-tigers-sell.md
new file mode 100644
index 000000000..3820203e8
--- /dev/null
+++ b/.changeset/few-tigers-sell.md
@@ -0,0 +1,5 @@
+---
+"@marigold/tsconfig": minor
+---
+
+feat: explicitly set strict rules in tsconfig
diff --git a/config/tsconfig/base.json b/config/tsconfig/base.json
index 7b5d2c8c8..69101aa5b 100644
--- a/config/tsconfig/base.json
+++ b/config/tsconfig/base.json
@@ -8,8 +8,14 @@
     "sourceMap": true,
     "allowSyntheticDefaultImports": true,
     "esModuleInterop": true,
-    "strict": true,
+    "alwaysStrict": true,
+    "strictNullChecks": true,
+    "strictBindCallApply": true,
     "strictFunctionTypes": false,
+    "strictPropertyInitialization": true,
+    "noImplicitAny": true,
+    "noImplicitThis": true,
+    "useUnknownInCatchVariables": true,
     "forceConsistentCasingInFileNames": true,
     "noUnusedLocals": true,
     "inlineSources": false,
diff --git a/docs/src/demos/components/TextField/index.ts b/docs/src/demos/components/TextField/index.ts
index e6a2bec04..5242df82b 100644
--- a/docs/src/demos/components/TextField/index.ts
+++ b/docs/src/demos/components/TextField/index.ts
@@ -4,3 +4,4 @@ export * from './textfield-req.demo';
 export * from './textfield-error.demo';
 export * from './textfield-number.demo';
 export * from './textfield-types.demo';
+export * from './textfield-group.demo';
diff --git a/docs/src/demos/components/TextField/textfield-group.demo.tsx b/docs/src/demos/components/TextField/textfield-group.demo.tsx
new file mode 100644
index 000000000..9e0d94c8b
--- /dev/null
+++ b/docs/src/demos/components/TextField/textfield-group.demo.tsx
@@ -0,0 +1,31 @@
+import { useState } from 'react';
+import { Stack, TextField, FieldGroup, Radio } from '@marigold/components';
+
+export const GroupTextField = () => {
+  const [value, setValue] = useState<string>('');
+  const error = value.length > 0 && !/^\d+$/.test(value);
+
+  return (
+    <FieldGroup labelWidth="20%">
+      <Stack space="small">
+        <TextField label="Name" />
+        <TextField
+          label="Promo Code"
+          description="You'll find your promo code on the back of your ticket."
+          errorMessage="Your promo code is not valid! Please review your input."
+          value={value}
+          onChange={setValue}
+          error={error}
+          inputMode="numeric"
+          pattern="[0-9]*"
+        />
+        <Radio.Group label="Choose">
+          <Radio value="1">One</Radio>
+          <Radio value="2">Two</Radio>
+          <Radio value="3">Three</Radio>
+          <Radio value="4">Four</Radio>
+        </Radio.Group>
+      </Stack>
+    </FieldGroup>
+  );
+};
diff --git a/docs/src/pages/components/label.mdx b/docs/src/pages/components/label.mdx
index 647a369c4..f31fcd1e8 100644
--- a/docs/src/pages/components/label.mdx
+++ b/docs/src/pages/components/label.mdx
@@ -48,6 +48,12 @@ import { Label } from '@marigold/components';
       description: 'Sets the label required.',
       default: 'false',
     },
+    {
+      property: 'labelWidth',
+      type: 'string',
+      description: 'Sets the label width.',
+      default: 'none',
+    },
     {
       property: '...',
       type: '',
diff --git a/docs/src/pages/components/text-field.mdx b/docs/src/pages/components/text-field.mdx
index f13d6cc86..089b65f6b 100644
--- a/docs/src/pages/components/text-field.mdx
+++ b/docs/src/pages/components/text-field.mdx
@@ -12,6 +12,8 @@ The `<TextField>` is a form component which allows user to enter text with a key
 
 It has many properties that it brings with it. You can set label for labeling the `<TextField>` itself, a `description` which behaves as help text. It is also possible to add error messages by adding the `error` and `errorMessage` prop. Another often used property is the `required` prop. You can also write all other HTML input properties down.
 
+The `<FieldGroup>` is a component to layout the label and the fieldbase itself. On that you can use `labelWidth` which is a string type to set the width of the label. Because the label is set to the left side in the core theme, it has only an impact on that.
+
 <Preview>
   <TextField label="Try me" description="You should really try this!" />
 </Preview>
@@ -150,10 +152,18 @@ The example shows how to set the `error` and `errorMessage` properties. If you f
 
 ```
 
-### Use different `input` methods
+### Use different input methods
 
 In this example you can see different types of inputs, like email, date or telephone number.
 
 ```tsx preview file=components/TextField/textfield-types.demo.tsx
 
 ```
+
+### Using with FieldGroup
+
+This example shows how to use the `<FieldGroup>` together with some form components like the `<TextField>` and `<Radio>`. It is build for the Core and has only impact of that.
+
+```tsx preview file=components/TextField/textfield-group.demo.tsx
+
+```
diff --git a/packages/components/src/FieldBase/FieldBase.stories.tsx b/packages/components/src/FieldBase/FieldBase.stories.tsx
index 9644d56b9..c65cc7569 100644
--- a/packages/components/src/FieldBase/FieldBase.stories.tsx
+++ b/packages/components/src/FieldBase/FieldBase.stories.tsx
@@ -1,6 +1,12 @@
 import React from 'react';
 import type { Meta, ComponentStory } from '@storybook/react';
 import { FieldBase } from './FieldBase';
+import { Select } from '../Select';
+import { FieldGroup } from './FieldGroup';
+import { TextField } from '../TextField';
+import { RadioGroup } from '../Radio/RadioGroup';
+import { Radio } from '../Radio';
+import { Checkbox, CheckboxGroup } from '../Checkbox';
 
 export default {
   title: 'Components/FieldBase',
@@ -54,3 +60,28 @@ export const Basic: ComponentStory<typeof FieldBase> = args => (
     <input type="text" />
   </FieldBase>
 );
+
+export const Complex: ComponentStory<typeof FieldBase> = args => (
+  <FieldGroup labelWidth="30%">
+    <FieldBase {...args} label="This is my Label">
+      <input type="text" />
+    </FieldBase>
+    <FieldBase {...args} label="This is my Label">
+      <input type="text" />
+    </FieldBase>
+    <TextField label="Hello TextField" />
+    <TextField label="Hello" description="my description" />
+    <Select label="Select me">
+      <Select.Option key="one">One</Select.Option>
+      <Select.Option key="two">Two</Select.Option>
+    </Select>
+    <RadioGroup label="Radios">
+      <Radio value="1">One</Radio>
+      <Radio value="2">Two</Radio>
+    </RadioGroup>
+    <CheckboxGroup label="Checkboxes">
+      <Checkbox value="1">One</Checkbox>
+      <Checkbox value="2">Two</Checkbox>
+    </CheckboxGroup>
+  </FieldGroup>
+);
diff --git a/packages/components/src/FieldBase/FieldBase.tsx b/packages/components/src/FieldBase/FieldBase.tsx
index 920582b7b..8fe3adde0 100644
--- a/packages/components/src/FieldBase/FieldBase.tsx
+++ b/packages/components/src/FieldBase/FieldBase.tsx
@@ -8,6 +8,7 @@ import {
 
 import { Label, LabelProps } from '../Label';
 import { HelpText } from '../HelpText';
+import { useFieldGroupContext } from './FieldGroup';
 export interface FieldBaseProps {
   children?: ReactNode;
   variant?: string;
@@ -51,6 +52,8 @@ export const FieldBase = ({
 
   const style = useComponentStyles('Field', { variant, size });
 
+  const { labelWidth } = useFieldGroupContext();
+
   return (
     <Box
       {...props}
@@ -67,6 +70,7 @@ export const FieldBase = ({
           required={required}
           variant={variant}
           size={size}
+          labelWidth={labelWidth}
           {...labelProps}
           {...stateProps}
         >
diff --git a/packages/components/src/FieldBase/FieldGroup.tsx b/packages/components/src/FieldBase/FieldGroup.tsx
new file mode 100644
index 000000000..defb4059b
--- /dev/null
+++ b/packages/components/src/FieldBase/FieldGroup.tsx
@@ -0,0 +1,21 @@
+import React from 'react';
+import { createContext, ReactNode, useContext } from 'react';
+
+export interface FieldGroupContextProps {
+  labelWidth?: string;
+}
+
+export const FieldGroupContext = createContext<FieldGroupContextProps>({});
+export const useFieldGroupContext = () => useContext(FieldGroupContext);
+
+export interface FieldGroupProps {
+  labelWidth?: string;
+  children: ReactNode;
+}
+export const FieldGroup = ({ labelWidth, children }: FieldGroupProps) => {
+  return (
+    <FieldGroupContext.Provider value={{ labelWidth }}>
+      {children}
+    </FieldGroupContext.Provider>
+  );
+};
diff --git a/packages/components/src/FieldBase/index.ts b/packages/components/src/FieldBase/index.ts
index 0100b6855..2aaf84c79 100644
--- a/packages/components/src/FieldBase/index.ts
+++ b/packages/components/src/FieldBase/index.ts
@@ -1,1 +1,2 @@
 export * from './FieldBase';
+export * from './FieldGroup';
diff --git a/packages/components/src/Label/Label.tsx b/packages/components/src/Label/Label.tsx
index 65e12e33f..bd0e63a31 100644
--- a/packages/components/src/Label/Label.tsx
+++ b/packages/components/src/Label/Label.tsx
@@ -14,6 +14,7 @@ export interface LabelProps extends ComponentProps<'label'> {
   variant?: string;
   size?: string;
   required?: boolean;
+  labelWidth?: string;
 }
 
 // Component
@@ -24,6 +25,7 @@ export const Label = ({
   children,
   variant,
   size,
+  labelWidth,
   ...props
 }: LabelProps) => {
   const styles = useComponentStyles('Label', { size, variant });
@@ -35,7 +37,10 @@ export const Label = ({
       // aria-required is set on the field and will already be announced,
       // so we don't need to add it here. BUT we need it for styling the required label, so this is needed.
       aria-required={required}
-      __baseCSS={{ display: 'flex' }}
+      __baseCSS={{
+        display: 'flex',
+        width: labelWidth,
+      }}
       css={styles}
     >
       {children}
diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts
index 932ae459d..5b911bc79 100644
--- a/packages/components/src/index.ts
+++ b/packages/components/src/index.ts
@@ -15,6 +15,7 @@ export * from './Columns';
 export * from './Container';
 export * from './Dialog';
 export * from './Divider';
+export * from './FieldBase';
 export * from './Footer';
 export * from './Header';
 export * from './Headline';
diff --git a/themes/theme-b2b/src/components/Field.style.ts b/themes/theme-b2b/src/components/Field.style.ts
new file mode 100644
index 000000000..ba9426097
--- /dev/null
+++ b/themes/theme-b2b/src/components/Field.style.ts
@@ -0,0 +1,7 @@
+import type { Theme } from '@marigold/components';
+
+export const Field: Theme['components']['Field'] = {
+  base: {
+    flexDirection: 'column',
+  },
+};
diff --git a/themes/theme-b2b/src/components/index.ts b/themes/theme-b2b/src/components/index.ts
index 61fa2d5e5..07a9cb4f4 100644
--- a/themes/theme-b2b/src/components/index.ts
+++ b/themes/theme-b2b/src/components/index.ts
@@ -4,6 +4,7 @@ export * from './Card.style';
 export * from './Checkbox.style';
 export * from './Dialog.style';
 export * from './Divider.style';
+export * from './Field.style';
 export * from './Headline.style';
 export * from './HelpText.style';
 export * from './Image.style';
diff --git a/themes/theme-core/src/components/Label.style.ts b/themes/theme-core/src/components/Label.style.ts
index 31f7d6f0b..fb13e53ae 100644
--- a/themes/theme-core/src/components/Label.style.ts
+++ b/themes/theme-core/src/components/Label.style.ts
@@ -2,6 +2,7 @@ import { Theme } from '@marigold/components';
 
 export const Label: Theme['components']['Label'] = {
   base: {
+    justifyContent: 'flex-end',
     fontSize: 'xxsmall',
     color: 'text',
     '&:disabled': {
diff --git a/themes/theme-unicorn/src/components/Field.style.ts b/themes/theme-unicorn/src/components/Field.style.ts
new file mode 100644
index 000000000..ba9426097
--- /dev/null
+++ b/themes/theme-unicorn/src/components/Field.style.ts
@@ -0,0 +1,7 @@
+import type { Theme } from '@marigold/components';
+
+export const Field: Theme['components']['Field'] = {
+  base: {
+    flexDirection: 'column',
+  },
+};
diff --git a/themes/theme-unicorn/src/components/index.ts b/themes/theme-unicorn/src/components/index.ts
index fc91eaf08..7f5ad38dc 100644
--- a/themes/theme-unicorn/src/components/index.ts
+++ b/themes/theme-unicorn/src/components/index.ts
@@ -4,6 +4,7 @@ export * from './Card.style';
 export * from './Checkbox.style';
 export * from './Dialog.style';
 export * from './Divider.style';
+export * from './Field.style';
 export * from './Headline.style';
 export * from './HelpText.style';
 export * from './Image.style';


Test patch:
diff --git a/packages/components/src/FieldBase/FieldGroup.test.tsx b/packages/components/src/FieldBase/FieldGroup.test.tsx
new file mode 100644
index 000000000..7984fe0e8
--- /dev/null
+++ b/packages/components/src/FieldBase/FieldGroup.test.tsx
@@ -0,0 +1,58 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+
+import { FieldBase } from './FieldBase';
+import { FieldGroup } from './FieldGroup';
+
+test('renders FieldGroup', () => {
+  render(
+    <FieldGroup>
+      <FieldBase label="Label" description="Description">
+        <input type="text" />
+      </FieldBase>
+    </FieldGroup>
+  );
+
+  // eslint-disable-next-line testing-library/no-node-access
+  const group = screen.getByText('Label').parentElement;
+  expect(group).toBeInTheDocument();
+});
+
+test('renders FieldGroup by default without label width', () => {
+  render(
+    <FieldGroup>
+      <FieldBase label="Label" description="Description">
+        <input type="text" />
+      </FieldBase>
+    </FieldGroup>
+  );
+
+  const label = screen.getByText('Label');
+  expect(label).not.toHaveAttribute('labelWidth');
+});
+
+test('renders FieldGroup  with label width', () => {
+  render(
+    <FieldGroup labelWidth="20px">
+      <FieldBase label="Label" description="Description">
+        <input type="text" />
+      </FieldBase>
+    </FieldGroup>
+  );
+
+  const label = screen.getByText('Label');
+  expect(label).toHaveStyle('width: 20px');
+});
+
+test('renders FieldGroups children', () => {
+  render(
+    <FieldGroup labelWidth="20px">
+      <FieldBase label="Label" description="Description">
+        <input type="text" />
+      </FieldBase>
+    </FieldGroup>
+  );
+
+  expect(screen.getByText('Label')).toBeInTheDocument();
+  expect(screen.getByText('Description')).toBeInTheDocument();
+});
diff --git a/packages/components/src/Label/Label.test.tsx b/packages/components/src/Label/Label.test.tsx
index 8ce8bab4d..77058c2d6 100644
--- a/packages/components/src/Label/Label.test.tsx
+++ b/packages/components/src/Label/Label.test.tsx
@@ -76,3 +76,15 @@ test('can render as <span>', () => {
   const label = screen.getByText(/label/);
   expect(label instanceof HTMLSpanElement).toBeTruthy();
 });
+
+test('accepts labelwidth', () => {
+  render(
+    <ThemeProvider theme={theme}>
+      <Label as="span" labelWidth="100px">
+        label
+      </Label>
+    </ThemeProvider>
+  );
+  const label = screen.getByText(/label/);
+  expect(label).toHaveStyle('width: 100px');
+});