Skip to content

Commit

Permalink
feat: Added form field component
Browse files Browse the repository at this point in the history
This PR introduces a Field component. It will enable us to build other form controls by providing styled form-elements.
  • Loading branch information
nicolechung authored Feb 16, 2023
2 parents a4d740e + 38e906e commit 44a2e65
Show file tree
Hide file tree
Showing 18 changed files with 495 additions and 46 deletions.
1 change: 1 addition & 0 deletions docs-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"@docfy/core": "^0.5.0",
"@docfy/ember": "^0.5.0",
"@ember/optional-features": "^2.0.0",
"@ember/string": "^3.0.1",
"@ember/test-helpers": "^2.8.1",
"@embroider/compat": "^2.0.0",
"@embroider/core": "^2.0.0",
Expand Down
22 changes: 22 additions & 0 deletions docs/components/field/demo/base-demo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
```hbs template
<Form::Field as |field|>
<field.Label for={{field.id}}>Label</field.Label>
<field.Hint id={{field.hintId}}>Extra information about the field</field.Hint>
<field.Control>
<input
class='border bg-basement text-titles-and-attributes p-1 rounded-sm'
id={{field.id}}
aria-invalid='true'
aria-describedby='{{field.hintId}} {{field.errorId}}'
...attributes
/>
</field.Control>
<field.Error id={{field.errorId}}>Error message</field.Error>
</Form::Field>
```

```js component
import Component from '@glimmer/component';

export default class extends Component {}
```
74 changes: 74 additions & 0 deletions docs/components/field/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Field

Field is a component to aid in creating form components. It provides a label, hint sections for things like help text, a control block, and an error section for rendering errors. It allows users to provide custom controls with a consistent form-element shell and is not opinionated on what underlying control element is used.

## Yielded Items

- `id`: A unique ID to provide to the control element `id` attribute and the Label component `for` attribute.
- `hintId`: A unique ID to provide to the control element `aria-describedby` attribute and the Hint component `id` attribute.
- `errorId`: A unique ID to provide to the control element `aria-describedby` or `aria-errormessage` attribute and the Error component `id` attribute.
- `Label`: Renders a `<label>` element. Form element label text is normally rendered here.
- `Hint`: Renders a `<div>` element. Help text or supplemental information is normally rendered here.
- `Control`: A control block where a form element is rendered, for example, an `<input>`, `<textarea>`, etc.
- `Error`: Renders a `<div>` element. Error information is normally rendered here.

## Accessibility

The Field component does not handle accessibility automatically for the label, hint, and error sections of the component; however, it does provide identifiers to assist here. Each code example on this page also shows how to take advantage of these IDs.

- [for](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/for)
- [aria-describedby](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-describedby)
- [aria-errormessage](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-errormessage)
- [aria-invalid](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-invalid)

## Optionally Rendering Components

The yielded components from Field can be optionally rendered by using the `{{#if}}` helper. The below example optionally renders the Hint and Error based on component arguments.

```hbs
<Form::Field as |field|>
<field.Label for={{field.id}}>{{@label}}</field.Label>
{{#if @helperText}}
<field.Hint id={{field.hintId}}>{{@helperText}}</field.Hint>
{{/if}}
<field.Control>
<input id={{field.id}} aria-describedby={{field.hintId}} aria-invalid={{if @error "true"}} aria-errormessage={{if @error field.errorId}}class='border-critical bg-blue' ...attributes />
</field.Control>
{{#if @error}}
<field.Error id={{field.errorId}}>{{@error}}</field.Error>
{{/if}}
</Form::Field>
```

### Attributes and Modifiers

Attributes are spread to each sub component of the Field via `...attributes`, so HTML attributes and Ember modifiers can be added.

```hbs
<Form::Field as |field|>
<field.Label
for={{field.id}}
class='mt-3'
data-test-label
{{on 'hover' this.onLabelHover}}
>First name</field.Label>
{{! other components below ...}}
</Form::Field>
```

### Ordering of Components

Ordering of the elements can be changed by adjusting the order of the children. For example, if it is preferred to put the hint block underneath the control.

```hbs
<Form::Field as |field|>
<field.Label for={{field.id}}>First name</field.Label>
<field.Control>
<input id={{field.id}} aria-describedby={{field.hintId}} ...attributes />
</field.Control>
<field.Hint id={{field.hintId}}>Hint text below the input</field.Hint>
</Form::Field>
```
10 changes: 6 additions & 4 deletions ember-toucan-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,9 @@
"@embroider/addon-dev": "^3.0.0",
"@glimmer/component": "^1.1.2",
"@glimmer/tracking": "^1.1.2",
"@glint/core": "^0.9.7",
"@glint/environment-ember-loose": "^0.9.7",
"@glint/template": "^0.9.7",
"@glint/core": "^1.0.0-beta.3",
"@glint/environment-ember-loose": "^1.0.0-beta.3",
"@glint/template": "^1.0.0-beta.3",
"@nullvoxpopuli/eslint-configs": "^3.0.4",
"@tsconfig/ember": "^2.0.0",
"@types/ember": "^4.0.0",
Expand Down Expand Up @@ -113,7 +113,8 @@
"type": "addon",
"main": "addon-main.cjs",
"app-js": {
"./components/button.js": "./dist/_app_/components/button.js"
"./components/button.js": "./dist/_app_/components/button.js",
"./components/form/field.js": "./dist/_app_/components/form/field.js"
}
},
"exports": {
Expand All @@ -122,6 +123,7 @@
"types": "./dist/*.d.ts",
"default": "./dist/*.js"
},
"./components/**/-private/*": null,
"./test-support": "./dist/test-support/index.js",
"./addon-main.js": "./addon-main.cjs"
},
Expand Down
9 changes: 4 additions & 5 deletions ember-toucan-core/rollup.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,15 @@ export default {
// These are the modules that users should be able to import from your
// addon. Anything not listed here may get optimized away.
addon.publicEntrypoints([
'components/**/*.js',
'index.js',
'template-registry.js',
'test-support/index.js',
// For our own build we treat all JS modules as entry points, to not cause rollup-plugin-ts to mess things up badly when trying to tree-shake TS declarations
// but the actual importable modules are further restricted by the package.json entry points!
'**/*.js',
]),

// These are the modules that should get reexported into the traditional
// "app" tree. Things in here should also be in publicEntrypoints above, but
// not everything in publicEntrypoints necessarily needs to go here.
addon.appReexports(['components/**/*.js']),
addon.appReexports(['components/*.js', 'components/form/*.js']),

// compile TypeScript to latest JavaScript, including Babel transpilation
typescript({
Expand Down
3 changes: 3 additions & 0 deletions ember-toucan-core/src/components/form/-private/control.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<div class="mt-1" ...attributes>
{{yield}}
</div>
11 changes: 11 additions & 0 deletions ember-toucan-core/src/components/form/-private/control.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import templateOnlyComponent from '@ember/component/template-only';

export interface ToucanFormControlComponentSignature {
Element: HTMLDivElement;
Args: {};
Blocks: {
default: [];
};
}

export default templateOnlyComponent<ToucanFormControlComponentSignature>();
22 changes: 22 additions & 0 deletions ember-toucan-core/src/components/form/-private/error.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<div class="type-xs-tight text-critical flex items-center mt-1" ...attributes>
{{! Icon taken from Tony's open source icon set, this is temporary! }}
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
class="w-3 h-3 mr-1"
>
<path
d="M12 3a9 9 0 11-6.364 2.636A8.972 8.972 0 0112 3zm0 4.7v5.2m0 3.39v.01"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg>

{{yield}}
</div>
11 changes: 11 additions & 0 deletions ember-toucan-core/src/components/form/-private/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import templateOnlyComponent from '@ember/component/template-only';

export interface ToucanFormErrorComponentSignature {
Element: HTMLDivElement;
Args: {};
Blocks: {
default: [];
};
}

export default templateOnlyComponent<ToucanFormErrorComponentSignature>();
1 change: 1 addition & 0 deletions ember-toucan-core/src/components/form/-private/hint.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div class="type-xs-tight text-body-and-labels" ...attributes>{{yield}}</div>
11 changes: 11 additions & 0 deletions ember-toucan-core/src/components/form/-private/hint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import templateOnlyComponent from '@ember/component/template-only';

export interface ToucanFormHintComponentSignature {
Element: HTMLDivElement;
Args: {};
Blocks: {
default: [];
};
}

export default templateOnlyComponent<ToucanFormHintComponentSignature>();
3 changes: 3 additions & 0 deletions ember-toucan-core/src/components/form/-private/label.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<label class="type-md-tight text-body-and-labels block" ...attributes>
{{yield}}
</label>
11 changes: 11 additions & 0 deletions ember-toucan-core/src/components/form/-private/label.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import templateOnlyComponent from '@ember/component/template-only';

export interface ToucanFormLabelComponentSignature {
Element: HTMLLabelElement;
Args: {};
Blocks: {
default: [];
};
}

export default templateOnlyComponent<ToucanFormLabelComponentSignature>();
13 changes: 13 additions & 0 deletions ember-toucan-core/src/components/form/field.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{{#let (unique-id) (unique-id) (unique-id) as |uniqueId hintId errorId|}}
{{yield
(hash
Label=this.Label
Hint=this.Hint
Control=this.Control
Error=this.Error
id=uniqueId
hintId=hintId
errorId=errorId
)
}}
{{/let}}
51 changes: 51 additions & 0 deletions ember-toucan-core/src/components/form/field.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import Component from '@glimmer/component';

import Control from './-private/control';
import Error from './-private/error';
import Hint from './-private/hint';
import Label from './-private/label';

interface ToucanFormFieldComponentSignature {
Element: null;
Args: {};
Blocks: {
default: [
{
/**
* ID to link the Label and underlying control element for screenreaders.
*
* - Provide this to the `id` attribute on the control element.
* - Provide this to the `for` attribute on the Label component.
*/
id: string;
/**
* A provided ID for element descriptors. Normally used to link the
* hint section with the control so that it is read by screenreaders.
*
* - Provide this to the `id` attribute on the Hint component.
* - Add this to the `aria-describedby` of the control element.
*/
hintId: string;
/**
* A provided ID for element descriptors. Normally used to link the
* error section with the control so that it is read by screenreaders.
*
* - Provide this to the `id` attribute on the Error component.
* - Add this to the `aria-describedby` or `aria-errormessage` of the control element.
*/
errorId: string;
Label: typeof Label;
Hint: typeof Hint;
Control: typeof Control;
Error: typeof Error;
}
];
};
}

export default class ToucanFormFieldComponent extends Component<ToucanFormFieldComponentSignature> {
Label = Label;
Hint = Hint;
Control = Control;
Error = Error;
}
Loading

0 comments on commit 44a2e65

Please sign in to comment.