Skip to content

Commit

Permalink
✨ pix-ui: create pix code
Browse files Browse the repository at this point in the history
Co-authored-by: Eric Lim <[email protected]>
  • Loading branch information
AndreiaPena and er-lim committed Feb 4, 2025
1 parent 3259c60 commit de00b90
Show file tree
Hide file tree
Showing 9 changed files with 334 additions and 1 deletion.
31 changes: 31 additions & 0 deletions addon/components/pix-code.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<div>
{{#if (has-block "label")}}
<PixLabel
@for={{this.id}}
@requiredLabel={{@requiredLabel}}
@subLabel={{@subLabel}}
@size={{@size}}
@screenReaderOnly={{@screenReaderOnly}}
>
{{yield to="label"}}
</PixLabel>
{{/if}}
<div>
<input
id={{this.id}}
class="pix-code"
style={{this.style}}
value={{@value}}
aria-required="{{if @requiredLabel true false}}"
required={{if @requiredLabel true false}}
maxlength={{this.length}}
minlength={{this.length}}
aria-describedby={{this.ariaDescribedBy}}
...attributes
/>

{{#if this.hasErrorMessage}}
<p id={{this.ariaDescribedBy}} class="pix-code__error-message">{{@errorMessage}}</p>
{{/if}}
</div>
</div>
24 changes: 24 additions & 0 deletions addon/components/pix-code.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { warn } from '@ember/debug';
import { htmlSafe } from '@ember/template';

import PixInputBase from './pix-input-base';

export default class PixCode extends PixInputBase {
constructor() {
super(...arguments);

this.prefix = 'pix-code';
}

get length() {
warn('PixCode: @length is required.', !['', null, undefined].includes(this.args.length), {
id: 'pix-ui.code.length.required',
});

return this.args.length || 1;
}

get style() {
return htmlSafe('--nb-characters:' + this.length);
}
}
37 changes: 37 additions & 0 deletions addon/styles/_pix-code.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
@use "pix-design-tokens/typography";
@use "component-state/form";

.pix-code {
@extend %pix-monospace;
@extend %pix-form-element-state;

$space-between-dashes: 0.6ch;
$total-width: calc(var(--nb-characters) * (1ch + $space-between-dashes));

display: inline-block;
box-sizing: content-box;
width: $total-width;
height: 3.125rem;
padding: 0 0.2ch 1px 0.5ch;
color: var(--pix-neutral-900);
font-size: 1.875rem;
letter-spacing: $space-between-dashes;
text-transform: uppercase;
background: repeating-linear-gradient(
90deg,
var(--pix-neutral-500) 0,
var(--pix-neutral-500) 1ch,
transparent 0,
transparent 1ch + $space-between-dashes
)
0 100% / calc($total-width - $space-between-dashes) 1px no-repeat;
background-position-x: 0.5ch;
background-position-y: 2.5ch;
border: solid 1px var(--pix-neutral-500);
border-radius: var(--pix-spacing-1x);
outline: none;
}

.pix-code__error-message {
@extend %pix-input-error-message;
}
1 change: 1 addition & 0 deletions addon/styles/addon.scss
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
@use 'pix-navigation-separator';
@use 'pix-app-layout';
@use 'pix-structure-switcher';
@use 'pix-code';


// at the end so it can override it's children scss
Expand Down
1 change: 1 addition & 0 deletions app/components/pix-code.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from '@1024pix/pix-ui/components/pix-code';
38 changes: 38 additions & 0 deletions app/stories/pix-code.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Meta, Story, ArgTypes } from '@storybook/blocks';

import * as ComponentStories from './pix-code.stories.js';

<Meta of={ComponentStories} />

# Pix Code

Une `PixCode` est un champ permettant de renseigner un code. Le nombre de caractères attendu est visible par le nombre de traits dans le champ.


## Without label

<Story of={ComponentStories.WithoutLabel} height={200} />

## With label (and subLabel)

<Story of={ComponentStories.WithLabel} height={200} />

## Error state (with error message)

<Story of={ComponentStories.Error} height={200} />

## Usage

```html
<PixCode
@length=6
@errorMessage="Une erreur est survenue"
@validationStatus="error"
>
<:label>Code campagne</:label>
</PixCode>
```

## Arguments

<ArgTypes story="Default" />
102 changes: 102 additions & 0 deletions app/stories/pix-code.stories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { hbs } from 'ember-cli-htmlbars';

export default {
title: 'Forms/Code',
argTypes: {
id: {
name: 'id',
description: 'Identifiant du champ permettant de lui attacher un label',
type: { name: 'string', required: true },
},
length: {
name: 'length',
description: 'Correspond au nombre de caractères attendu dans le champ',
type: { name: 'number', required: true },
table: {
defaultValue: { summary: 1 },
},
},
validationStatus: {
name: 'validationStatus',
description:
"Définit l'état du champ, neutre par défaut ou en erreur selon l'action de l'utilisateur",
type: { name: 'string', required: false },
options: ['default', 'error'],
control: { type: 'select' },
},
errorMessage: {
name: 'errorMessage',
description:
"Affiche le message d'erreur donné. Doit s'accompagner du paramètre validationStatus en 'error'",
type: { name: 'string', required: false },
},
label: {
name: 'label',
description: 'Le label du champ',
type: { name: 'string', required: false },
table: {
type: { summary: 'string' },
},
},
subLabel: {
name: 'subLabel',
description: 'Un descriptif complétant le label',
type: { name: 'string', required: false },
},
requiredLabel: {
name: 'requiredLabel',
description: 'Label indiquant que le champ est requis.',
type: { name: 'string', required: false },
table: {
type: { summary: 'string' },
},
},
screenReaderOnly: {
name: 'screenReaderOnly',
description: "Permet de rendre le label lisible uniquement par les lecteurs d'écran",
control: { type: 'boolean' },
type: { name: 'boolean', required: false },
table: {
type: { summary: 'boolean' },
defaultValue: { summary: false },
},
},
},
};

const Template = (args) => {
return {
template: hbs`<PixCode
@length={{this.length}}
@errorMessage={{this.errorMessage}}
@validationStatus={{this.validationStatus}}
@label={{this.label}}
@requiredLabel={{this.requiredLabel}}
@subLabel={{this.subLabel}}
@screenReaderOnly={{this.screenReaderOnly}}
>
<:label>{{this.label}}</:label>
</PixCode>`,
context: args,
};
};

export const WithoutLabel = Template.bind({});
WithoutLabel.args = {
length: 10,
};

export const WithLabel = Template.bind({});
WithLabel.args = {
length: 8,
label: 'Code de vérification',
subLabel: 'Exemple: P-XXXXXXXX',
};

export const Error = Template.bind({});
Error.args = {
length: 6,
label: 'Code de certification',
errorMessage: "un message d'erreur",
validationStatus: 'error',
};
2 changes: 1 addition & 1 deletion app/stories/pix-input-code.stories.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { hbs } from 'ember-cli-htmlbars';

export default {
title: 'Forms/Code',
title: 'Forms/Input Code',
argTypes: {
ariaLabel: {
name: 'ariaLabel',
Expand Down
99 changes: 99 additions & 0 deletions tests/integration/components/pix-code-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { render } from '@1024pix/ember-testing-library';
import { fillIn } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { setupRenderingTest } from 'ember-qunit';
import { module, test } from 'qunit';
import sinon from 'sinon';

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

test('it renders the default PixCode', async function (assert) {
// given
const screen = await render(
hbs`<PixCode @length={{10}}><:label>Code de vérification</:label></PixCode>`,
);

// when
await fillIn(screen.getByRole('textbox', { name: 'Code de vérification' }), 'P-123VALID');

// then
assert.contains('P-123VALID');
});

test('it should be possible to provide the expected number of length in the field', async function (assert) {
// given
const screen = await render(
hbs`<PixCode @length={{14}}><:label>Code de vérification</:label></PixCode>`,
);

// when
await fillIn(screen.getByRole('textbox', { name: 'Code de vérification' }), 'P-123VALID');

// then
assert
.dom(screen.getByRole('textbox', { name: 'Code de vérification' }))
.hasAttribute('maxLength', '14');
assert
.dom(screen.getByRole('textbox', { name: 'Code de vérification' }))
.hasAttribute('minLength', '14');
});

test('it should be possible to give an extra information to input', async function (assert) {
// given & when
const screen = await render(
hbs`<PixCode @length={{10}} @subLabel='exemple: P-1234'><:label>Code de vérification</:label></PixCode>`,
);

// then
assert.ok(screen.getByRole('textbox', { name: 'Code de vérification exemple: P-1234' }));
});

test('it should be possible to give an error message to input', async function (assert) {
// given & when
const screen = await render(
hbs`<PixCode
@errorMessage='Seul les caractères alphanumériques sont autorisés'
@validationStatus='error'
><:label>Code de vérification</:label></PixCode>`,
);

// then
assert.dom(screen.getByText('Seul les caractères alphanumériques sont autorisés')).exists();
});

test('it should be possible to make "pixCode" required', async function (assert) {
// given & when
const screen = await render(
hbs`<PixCode @length={{10}} @requiredLabel='Champ obligatoire'><:label>Code de vérification</:label></PixCode>`,
);

// then
const requiredInput = screen.getByLabelText('Code de vérification *');
assert.dom(requiredInput).isRequired();
});

module('@length', function (hooks) {
let warnStub;

hooks.beforeEach(function () {
warnStub = sinon.stub(console, 'warn');
});

hooks.afterEach(function () {
warnStub.restore();
});

['', null, undefined].forEach(function (testCase) {
test(`it should warn when @length="${testCase}"`, async function (assert) {
// given
this.set('length', testCase);
await render(
hbs`<PixCode @length={{this.length}} @requiredLabel='Champ obligatoire'><:label>Code de vérification</:label></PixCode>`,
);

assert.ok(warnStub.calledWithExactly('WARNING: PixCode: @length is required.'));
});
});
});
});

0 comments on commit de00b90

Please sign in to comment.