Skip to content

Commit

Permalink
feat: Add Fieldset component
Browse files Browse the repository at this point in the history
  • Loading branch information
ynotdraw committed Mar 3, 2023
1 parent 0912a5a commit 4bcda86
Show file tree
Hide file tree
Showing 7 changed files with 278 additions and 0 deletions.
16 changes: 16 additions & 0 deletions docs/components/fieldset/demo/base-demo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
```hbs template
<Form::Fieldset
@label='Label'
@hint='Extra information about the fieldset'
@error='Error message'
>
<p class='text-body-and-labels text-xs m-0 italic'>~Fieldset components render
here!~</p>
</Form::Fieldset>
```

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

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

Fieldset is a component to aid in creating form components that require an underlying `<fieldset>` and `<legend>`. It is similar to Field, in that it provides an opinionated shell for building other components such as checkbox groups and radio groups.

## Label

Provide a string to `@label` to render the text into the `<legend>` of the Fieldset. This is required.

```hbs
<Form::Fieldset @label='Label' />
```

## Hint

Provide a string to `@hint` to render the text into the Hint section of the Fieldset. This is optional.

```hbs
<Form::Fieldset @label='Label' @hint='Hint' />
```

## Error

Provide a string to `@error` to render the text into the Error section of the Fieldset. This is optional.

```hbs
<Form::Fieldset @label='Label' @error='Error' />
```

## Disabled State

Set the `@isDisabled` argument to disable the fieldset. When disabled, all form controls that are descendants of the fieldset, are disabled, meaning they are not editable and won't be submitted along with the form. Learn more via the [fieldset documentation](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/fieldset#attributes).

```hbs
<Form::Fieldset @label='Label' @isDisabled={{true}}>
<!-- This is now disabled as well -->
<input />
</Form::Fieldset>
```

## Attributes and Modifiers

Consumers have direct access to the underlying [fieldset element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/fieldset), so all attributes are supported.

```hbs
<Form::Fieldset @label='Label' name='my-checkboxes' data-fieldset />
```

## Test Selectors

### Root Element

The wrapping element is a `<fieldset>` and attributes are spread directly on it as mentioned above. Due to that, one can target the fieldset with any data attribute.

```hbs
<Form::Fieldset @label='Label' data-fieldset />
```

### Label

Target the label element via `data-label`.

### Hint

Target the hint block via `data-hint`.

### Wrapping Content Container

The `yield` is wrapped in a div container that can be targeted with `data-control`.

### Error

Target the error block via `data-error`.

## All UI States

<div class="flex flex-col space-y-4" style="max-width: 14rem">
<Form::Fieldset @label='Label'>

<p class='text-body-and-labels text-xs m-0 italic'>~Fieldset components render here!~</p>
</Form::Fieldset>

<Form::Fieldset @label='Label' @hint="With hint text">

<p class='text-body-and-labels text-xs m-0 italic'>~Fieldset components render here!~</p>
</Form::Fieldset>

<Form::Fieldset @label='Label' @error="With error">

<p class='text-body-and-labels text-xs m-0 italic'>~Fieldset components render here!~</p>
</Form::Fieldset>

<Form::Fieldset @label='Label' @hint="With hint text" @error="With error">

<p class='text-body-and-labels text-xs m-0 italic'>~Fieldset components render here!~</p>
</Form::Fieldset>
</div>
1 change: 1 addition & 0 deletions ember-toucan-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@
"./components/form/controls/checkbox.js": "./dist/_app_/components/form/controls/checkbox.js",
"./components/form/controls/textarea.js": "./dist/_app_/components/form/controls/textarea.js",
"./components/form/field.js": "./dist/_app_/components/form/field.js",
"./components/form/fieldset.js": "./dist/_app_/components/form/fieldset.js",
"./components/form/textarea-field.js": "./dist/_app_/components/form/textarea-field.js"
}
},
Expand Down
27 changes: 27 additions & 0 deletions ember-toucan-core/src/components/form/fieldset.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<Form::Field as |field|>
<fieldset
aria-describedby="{{if @error field.errorId}} {{if @hint field.hintId}}"
disabled={{@isDisabled}}
...attributes
>
<legend
class="type-md-tight text-body-and-labels block"
data-label
>{{@label}}</legend>

{{#if @hint}}
<field.Hint id={{field.hintId}} data-hint>{{@hint}}</field.Hint>
{{/if}}

<div
class="mt-2 flex flex-col rounded-sm {{if @error 'shadow-error-outline'}}"
data-control
>
{{yield}}
</div>

{{#if @error}}
<field.Error id={{field.errorId}} data-error>{{@error}}</field.Error>
{{/if}}
</fieldset>
</Form::Field>
26 changes: 26 additions & 0 deletions ember-toucan-core/src/components/form/fieldset.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import Component from '@glimmer/component';
import { assert } from '@ember/debug';

interface ToucanFormFieldsetComponentSignature {
Element: HTMLFieldSetElement;
Args: {
error?: string;
hasError?: boolean;
hint?: string;
isDisabled?: boolean;
label: string;
};
Blocks: {
default: [];
};
}

export default class ToucanFormFieldComponent extends Component<ToucanFormFieldsetComponentSignature> {
constructor(
owner: unknown,
args: ToucanFormFieldsetComponentSignature['Args']
) {
assert('A "@label" argument is required', args.label);
super(owner, args);
}
}
2 changes: 2 additions & 0 deletions ember-toucan-core/src/template-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import type CheckboxFieldComponent from './components/form/checkbox-field';
import type CheckboxConrolComponent from './components/form/controls/checkbox';
import type TextareaControlComponent from './components/form/controls/textarea';
import type FieldComponent from './components/form/field';
import type FieldsetComponent from './components/form/fieldset';
import type TextareaFieldComponent from './components/form/textarea-field';

export default interface Registry {
Button: typeof ButtonComponent;
'Form::Field': typeof FieldComponent;
'Form::Fieldset': typeof FieldsetComponent;
'Form::CheckboxField': typeof CheckboxFieldComponent;
'Form::TextareaField': typeof TextareaFieldComponent;
'Form::Controls::Checkbox': typeof CheckboxConrolComponent;
Expand Down
110 changes: 110 additions & 0 deletions test-app/tests/integration/components/fieldset-test.gts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/* eslint-disable no-undef -- Until https://github.com/ember-cli/eslint-plugin-ember/issues/1747 is resolved... */
/* eslint-disable simple-import-sort/imports,padding-line-between-statements,decorator-position/decorator-position -- Can't fix these manually, without --fix working in .gts */

import { find, render, setupOnerror } from '@ember/test-helpers';
import { module, test } from 'qunit';

import Fieldset from '@crowdstrike/ember-toucan-core/components/form/fieldset';
import { setupRenderingTest } from 'test-app/tests/helpers';

module('Integration | Component | Fieldset', function (hooks) {
setupRenderingTest(hooks);

test('it renders', async function (assert) {
await render(<template>
<Fieldset @label="Label" data-fieldset />
</template>);

assert.dom('[data-label]').hasText('Label');

assert
.dom('[data-hint]')
.doesNotExist(
'Expected hint block not to be displayed as a hint was not provided'
);

assert
.dom('[data-error]')
.doesNotExist(
'Expected error block not to be displayed as an error was not provided'
);

assert.dom('[data-control]').hasNoClass('shadow-error-outline');
});

test('it renders with a hint', async function (assert) {
await render(<template>
<Fieldset @label="Label" @hint="Hint text" data-fieldset />
</template>);

assert.dom('[data-hint]').hasText('Hint text');
assert.dom('[data-hint]').hasAttribute('id');
assert.dom('[data-fieldset]').hasAttribute('aria-describedby');
});

test('it renders with an error', async function (assert) {
await render(<template>
<Fieldset @label="Label" @error="Error text" data-fieldset />
</template>);

assert.dom('[data-error]').hasText('Error text');
assert.dom('[data-error]').hasAttribute('id');

assert.dom('[data-fieldset]').hasAttribute('aria-describedby');

assert.dom('[data-control]').hasClass('shadow-error-outline');
});

test('it sets aria-describedby when both a hint and error are provided', async function (assert) {
await render(<template>
<Fieldset
@label="Label"
@error="Error text"
@hint="Hint text"
data-fieldset
/>
</template>);

let errorId = find('[data-error]')?.getAttribute('id') || '';
assert.ok(errorId, 'Expected errorId to be truthy');

let hintId = find('[data-hint]')?.getAttribute('id') || '';
assert.ok(hintId, 'Expected hintId to be truthy');

assert
.dom('[data-fieldset]')
.hasAttribute('aria-describedby', `${errorId} ${hintId}`);
});

test('it disables the fieldset using `@isDisabled`', async function (assert) {
await render(<template>
<Fieldset @label="Label" @isDisabled={{true}} data-fieldset />
</template>);

assert.dom('[data-fieldset]').isDisabled();
});

test('it spreads attributes to the underlying fieldset', async function (assert) {
await render(<template>
<Fieldset @label="Label" form="form-id" data-fieldset />
</template>);

assert.dom('[data-fieldset]').hasAttribute('form', 'form-id');
});

test('it throws an assertion error if no `@label` is provided', async function (assert) {
assert.expect(1);

setupOnerror((e: Error) => {
assert.ok(
e.message.includes('A "@label" argument is required'),
'Expected assertion error message'
);
});

await render(<template>
{{! @glint-expect-error: we are not providing @label, so this is expected }}
<Fieldset />
</template>);
});
});

0 comments on commit 4bcda86

Please sign in to comment.