diff --git a/addon/components/pix-table-column.hbs b/addon/components/pix-table-column.hbs
index 326124ef5..dbff02b4e 100644
--- a/addon/components/pix-table-column.hbs
+++ b/addon/components/pix-table-column.hbs
@@ -1,6 +1,16 @@
{{#if this.displayHeader}}
-
- {{yield to="header"}}
+ |
+
|
{{else}}
diff --git a/addon/components/pix-table-column.js b/addon/components/pix-table-column.js
index f058a76bf..466b97f15 100644
--- a/addon/components/pix-table-column.js
+++ b/addon/components/pix-table-column.js
@@ -6,10 +6,77 @@ export default class PixTableColumn extends Component {
return this.args.context === 'header';
}
+ get type() {
+ return this.args.type ?? 'text';
+ }
+
+ get sortable() {
+ return Boolean(this.args.onSort);
+ }
+
+ get sortOrder() {
+ if (this.args.sortOrder === undefined) {
+ return undefined;
+ }
+ const correctSortOrders = ['asc', 'desc', null];
+ warn(
+ 'PixTableColumn: you need to provide a valid sortOrder',
+ correctSortOrders.includes(this.args.sortOrder),
+ {
+ id: 'pix-ui.table-column.sortOrder.not-valid',
+ },
+ );
+ return this.args.sortOrder;
+ }
+
+ get iconName() {
+ const isText = this.type === 'text';
+ if (!this.sortOrder) {
+ return isText ? 'sortAz' : 'sort';
+ }
+ if (this.sortOrder === 'asc') {
+ return isText ? 'sortAzAsc' : 'sortAsc';
+ }
+ return isText ? 'sortAzDesc' : 'sortDesc';
+ }
+
+ get iconLabel() {
+ warn(
+ 'PixTableColumn: parameters `@ariaLabelDefaultSort`, `@ariaLabelSortDesc` and `@ariaLabelSortAsc` are required for sort buttons',
+ ![
+ this.args.ariaLabelDefaultSort,
+ this.args.ariaLabelSortDesc,
+ this.args.ariaLabelSortAsc,
+ ].includes(undefined),
+ {
+ id: 'pix-ui.pix-table-column.sortAriaLabels.required',
+ },
+ );
+ if (!this.sortOrder) {
+ return this.args.ariaLabelDefaultSort;
+ }
+ if (this.sortOrder === 'asc') {
+ return this.args.ariaLabelSortDesc;
+ }
+ return this.args.ariaLabelSortAsc;
+ }
+
+ get ariaSort() {
+ if (!this.sortable) {
+ return undefined;
+ }
+ if (!this.sortOrder) {
+ return 'none';
+ }
+ if (this.sortOrder === 'asc') {
+ return 'ascending';
+ }
+ return 'descending';
+ }
+
get typeClass() {
const correctTypes = ['number', 'text'];
- const type = this.args.type ?? 'text';
- warn('PixTableColumn: you need to provide a valid type', correctTypes.includes(type), {
+ warn('PixTableColumn: you need to provide a valid type', correctTypes.includes(this.type), {
id: 'pix-ui.table-column.type.incorrect',
});
if (this.args.type === 'number') {
diff --git a/addon/styles/_pix-table.scss b/addon/styles/_pix-table.scss
index 8329a1055..158900375 100644
--- a/addon/styles/_pix-table.scss
+++ b/addon/styles/_pix-table.scss
@@ -32,8 +32,15 @@
}
}
+ .pix-table-header-container {
+ display: flex;
+ gap: var(--pix-spacing-1x);
+ align-items: center;
+ }
+
th {
text-align: start;
+ vertical-align: middle;
}
td, th {
diff --git a/app/stories/pix-table-column.mdx b/app/stories/pix-table-column.mdx
index 3fadcbebb..269cca68f 100644
--- a/app/stories/pix-table-column.mdx
+++ b/app/stories/pix-table-column.mdx
@@ -40,6 +40,48 @@ Une colonne d'un [PixTable](/docs/data-display-table--docs), gère l'affichage d
```
+## Tri
+
+
+
+```html
+
+ <:columns as |row context|>
+
+ <:header>
+ Nom
+
+ <:cell>
+ {{row.name}}
+
+
+
+ <:header>
+ Age
+
+ <:cell>
+ {{row.age}}
+
+
+
+
+```
+
## Arguments
diff --git a/app/stories/pix-table-column.stories.js b/app/stories/pix-table-column.stories.js
index f363ee205..19e880079 100644
--- a/app/stories/pix-table-column.stories.js
+++ b/app/stories/pix-table-column.stories.js
@@ -8,6 +8,43 @@ export default {
description: 'Propriété a récupérer depuis le block element `<:columns>` du PixTable parent.',
type: { name: 'privé', required: true },
},
+ onSort: {
+ name: 'onSort',
+ description:
+ "Fonction appelée en cas de clic sur le bouton de tri d'une colonne. Sa présence détermine si le bouton de tri est affiché ou non. Le tri est à implémenter soi-même.",
+ type: { name: 'function', required: false },
+ },
+ sortOrder: {
+ name: 'sortOrder',
+ description:
+ "Statut du tri de la colonne. À gérer du côté de l'application. ⚠️ Obligatoire si `@onSort` est utilisé ⚠️",
+ options: ['asc', 'desc', null],
+ control: {
+ type: 'select',
+ },
+ type: {
+ name: '"asc" | "desc" | null',
+ required: false,
+ },
+ },
+ ariaLabelDefaultSort: {
+ name: 'ariaLabelDefaultSort',
+ description:
+ "Label du bouton de tri, lorsqu'aucun tri n'est appliqué. ⚠️ Obligatoire si `@onSort` est utilisé ⚠️",
+ type: { name: 'string', required: false },
+ },
+ ariaLabelSortAsc: {
+ name: 'ariaLabelSortAsc',
+ description:
+ 'Label du bouton de tri (pour trier en ordre ascendant), lorsque le tri descendant est appliqué. ⚠️ Obligatoire si `@onSort` est utilisé ⚠️',
+ type: { name: 'string', required: false },
+ },
+ ariaLabelSortDesc: {
+ name: 'ariaLabelSortDesc',
+ description:
+ 'Label du bouton de tri (pour trier en ordre descendant), lorsque le tri ascendant est appliqué. ⚠️ Obligatoire si `@onSort` est utilisé ⚠️',
+ type: { name: 'string', required: false },
+ },
type: {
defaultValue: {
summary: 'text',
@@ -74,3 +111,64 @@ Default.args = {
},
],
};
+
+const TemplateSort = (args) => {
+ return {
+ template: hbs`
+ <:columns as |row context|>
+
+ <:header>
+ Nom
+
+ <:cell>
+ {{row.name}}
+
+
+
+ <:header>
+ Age
+
+ <:cell>
+ {{row.age}}
+
+
+
+`,
+ context: args,
+ };
+};
+
+export const Sorted = TemplateSort.bind({});
+Sorted.args = {
+ caption: 'Description du tableau',
+ data: [
+ {
+ name: 'jean',
+ age: 15,
+ },
+ {
+ name: 'brian',
+ age: 25,
+ },
+ ],
+ sort() {},
+ sortOrder: 'asc',
+ ariaLabelDefaultSort: 'click pour trier',
+ ariaLabelSortAsc: 'click pour trier en ordre ascendant',
+ ariaLabelSortDesc: 'click pour trier en ordre descendant',
+};
diff --git a/app/stories/pix-table.stories.js b/app/stories/pix-table.stories.js
index 50b316d5f..91b05668a 100644
--- a/app/stories/pix-table.stories.js
+++ b/app/stories/pix-table.stories.js
@@ -93,4 +93,7 @@ Default.args = {
age: 25,
},
],
+ onNameSort: () => {
+ alert('Fonctionnalité seulement disponible en local sur dummy');
+ },
};
diff --git a/tests/dummy/app/controllers/table-page.js b/tests/dummy/app/controllers/table-page.js
new file mode 100644
index 000000000..d2a6d4337
--- /dev/null
+++ b/tests/dummy/app/controllers/table-page.js
@@ -0,0 +1,59 @@
+import Controller from '@ember/controller';
+import { tracked } from '@glimmer/tracking';
+import { action } from '@ember/object';
+
+export default class TablePage extends Controller {
+ @tracked
+ nameSortOrder = null;
+ @tracked
+ numSortOrder = null;
+
+ variant = 'orga';
+
+ @tracked
+ data = [
+ {
+ name: 'jean',
+ description: 'fort au jungle speed',
+ age: 15,
+ },
+ {
+ name: 'brian',
+ description: 'travail au peach pit',
+ age: 25,
+ },
+ ];
+
+ caption = 'Titre de mon tableau';
+
+ @action
+ onNameSort() {
+ this.resetOrders('name');
+ if (this.nameSortOrder === 'asc') {
+ this.data = this.data.sort((a, b) => b.name.localeCompare(a.name));
+ this.nameSortOrder = 'desc';
+ } else {
+ this.data = this.data.sort((a, b) => a.name.localeCompare(b.name));
+ this.nameSortOrder = 'asc';
+ }
+ }
+
+ @action
+ onNumSort() {
+ this.resetOrders('num');
+ if (this.numSortOrder === 'asc') {
+ this.data = this.data.sort((a, b) => b.age - a.age);
+ this.numSortOrder = 'desc';
+ } else {
+ this.data = this.data.sort((a, b) => a.age - b.age);
+ this.numSortOrder = 'asc';
+ }
+ }
+
+ resetOrders(except) {
+ for (const key of ['num', 'name']) {
+ if (key === except) continue;
+ this[`${key}SortOrder`] = null;
+ }
+ }
+}
diff --git a/tests/dummy/app/router.js b/tests/dummy/app/router.js
index a7be35f91..0a32ff0c3 100644
--- a/tests/dummy/app/router.js
+++ b/tests/dummy/app/router.js
@@ -13,4 +13,5 @@ Router.map(function () {
this.route('select-page', { path: '/select' });
this.route('sidebar-page', { path: '/sidebar' });
this.route('tooltip-page', { path: '/tooltip' });
+ this.route('table-page', { path: '/table' });
});
diff --git a/tests/dummy/app/templates/application.hbs b/tests/dummy/app/templates/application.hbs
index bb6abff08..7176505f3 100644
--- a/tests/dummy/app/templates/application.hbs
+++ b/tests/dummy/app/templates/application.hbs
@@ -14,6 +14,7 @@
select
Sidebar
tooltip
+ Table
Documentation
Centre
diff --git a/tests/dummy/app/templates/table-page.hbs b/tests/dummy/app/templates/table-page.hbs
new file mode 100644
index 000000000..5bb682d99
--- /dev/null
+++ b/tests/dummy/app/templates/table-page.hbs
@@ -0,0 +1,51 @@
+
+ <:columns as |row context|>
+
+ <:header>
+ Nom
+
+ <:cell>
+ {{row.name}}
+
+
+
+ <:header>
+ Description
+
+ <:cell>
+ {{row.description}}
+
+
+
+ <:header>
+ Age
+
+ <:cell>
+ {{row.age}}
+
+
+
+ <:header>
+ Info
+
+ <:cell>
+
+
+
+
+
+{{! template-lint-disable no-forbidden-elements }}
+
\ No newline at end of file
diff --git a/tests/integration/components/pix-table-test.js b/tests/integration/components/pix-table-test.js
index 23b5592dd..5975755da 100644
--- a/tests/integration/components/pix-table-test.js
+++ b/tests/integration/components/pix-table-test.js
@@ -2,6 +2,7 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@1024pix/ember-testing-library';
+import { click } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import EmberDebug from '@ember/debug';
import sinon from 'sinon';
@@ -19,7 +20,12 @@ module('Integration | Component | table', function (hooks) {
{
name: 'brian',
description: 'travail au peach pit',
- age: 25,
+ age: 14,
+ },
+ {
+ name: 'zoé',
+ description: 'travail aux affaires non classées',
+ age: 70,
},
];
});
@@ -119,6 +125,146 @@ module('Integration | Component | table', function (hooks) {
});
});
+ module('#sort', function () {
+ test('it should call @onSort on click', async function (assert) {
+ // given
+ const sortStub = sinon.stub();
+ this.onSort = sortStub;
+
+ const arialLabelDefaultSort = 'default label sort';
+ this.arialLabelDefaultSort = arialLabelDefaultSort;
+
+ // when
+
+ const screen = await render(
+ hbs`
+ <:columns as |row context|>
+
+ <:header>
+ Nom
+
+ <:cell>
+ {{row.name}}
+
+
+
+`,
+ );
+
+ // then
+ await click(await screen.getByRole('button', { name: arialLabelDefaultSort }));
+ assert.ok(sortStub.calledOnce);
+ });
+
+ test('it should display `ariaLabelSortAsc` when sortOrder is `desc`', async function (assert) {
+ // given
+ const sortStub = sinon.stub();
+ this.onSort = sortStub;
+
+ this.sortOrder = 'desc';
+
+ const ariaLabelSortAsc = "clicker pour trié dans l'ordre desc";
+ this.ariaLabelSortAsc = ariaLabelSortAsc;
+
+ // when
+
+ const screen = await render(
+ hbs`
+ <:columns as |row context|>
+
+ <:header>
+ Nom
+
+ <:cell>
+ {{row.name}}
+
+
+
+`,
+ );
+
+ // then
+ assert.ok(await screen.getByRole('button', { name: ariaLabelSortAsc }));
+ });
+
+ test('it should display `ariaLabelSortDesc` when sortOrder is `asc`', async function (assert) {
+ // given
+ const sortStub = sinon.stub();
+ this.onSort = sortStub;
+
+ this.sortOrder = 'asc';
+
+ const ariaLabelSortDesc = "clicker pour trié dans l'ordre asc";
+ this.ariaLabelSortDesc = ariaLabelSortDesc;
+
+ // when
+
+ const screen = await render(
+ hbs`
+ <:columns as |row context|>
+
+ <:header>
+ Nom
+
+ <:cell>
+ {{row.name}}
+
+
+
+`,
+ );
+
+ // then
+ assert.ok(await screen.getByRole('button', { name: ariaLabelSortDesc }));
+ });
+
+ test('it should not display sortlabel when `@onSort` is not provided', async function (assert) {
+ // given
+ const arialLabelDefaultSort = 'default label sort';
+ this.arialLabelDefaultSort = arialLabelDefaultSort;
+
+ // when
+ const screen = await render(
+ hbs`
+ <:columns as |row context|>
+
+ <:header>
+ Nom
+
+ <:cell>
+ {{row.name}}
+
+
+
+`,
+ );
+
+ // then
+ assert.notOk(await screen.queryByRole('button', { name: arialLabelDefaultSort }));
+ });
+ });
+
module('#warn', function (hooks) {
let sandbox;
hooks.beforeEach(function () {
@@ -132,7 +278,10 @@ module('Integration | Component | table', function (hooks) {
test('it should warn when @variant is incorrect', async function (assert) {
// when
- await render(hbs``);
+ this.data = [];
+ await render(
+ hbs``,
+ );
// then
assert.ok(
@@ -154,9 +303,10 @@ module('Integration | Component | table', function (hooks) {
);
});
- test('it should warn when @caption is not provided provided', async function (assert) {
+ test('it should warn when @caption is not provided', async function (assert) {
// when
- await render(hbs``);
+ this.data = [];
+ await render(hbs``);
// then
assert.ok(
@@ -170,5 +320,89 @@ module('Integration | Component | table', function (hooks) {
}),
);
});
+
+ test('it should warn when @sortOrder is incorrect', async function (assert) {
+ // when
+ this.data = [];
+ this.onSort = () => {};
+ await render(
+ hbs`
+ <:columns as |row context|>
+
+
+`,
+ );
+
+ // then
+ assert.ok(
+ EmberDebug.warn
+ .getCalls()
+ .find((call) => {
+ return call.args[2].id === 'pix-ui.table-column.sortOrder.not-valid';
+ })
+ .calledWith('PixTableColumn: you need to provide a valid sortOrder', false, {
+ id: 'pix-ui.table-column.sortOrder.not-valid',
+ }),
+ );
+ });
+
+ [
+ {
+ ariaLabelDefaultSort: 'tri',
+ ariaLabelSortDesc: 'tri',
+ ariaLabelSortAsc: undefined,
+ },
+ {
+ ariaLabelDefaultSort: 'tri',
+ ariaLabelSortDesc: undefined,
+ ariaLabelSortAsc: 'tri',
+ },
+ {
+ ariaLabelDefaultSort: undefined,
+ ariaLabelSortDesc: 'tri',
+ ariaLabelSortAsc: 'tri',
+ },
+ ].forEach(function (sortAriaLabels) {
+ const [missingLabel] = Object.entries(sortAriaLabels).find(([, value]) => !value);
+ test(`it should warn when ${missingLabel} is not provided`, async function (assert) {
+ // when
+ this.data = [];
+ this.onSort = () => {};
+ this.ariaLabelDefaultSort = sortAriaLabels.ariaLabelDefaultSort;
+ this.ariaLabelSortDesc = sortAriaLabels.ariaLabelSortDesc;
+ this.ariaLabelSortAsc = sortAriaLabels.ariaLabelSortAsc;
+
+ await render(hbs`
+ <:columns as |row context|>
+
+
+`);
+
+ // then
+ assert.ok(
+ EmberDebug.warn
+ .getCalls()
+ .find((call) => {
+ return (
+ call.args[0] ===
+ 'PixTableColumn: parameters `@ariaLabelDefaultSort`, `@ariaLabelSortDesc` and `@ariaLabelSortAsc` are required for sort buttons'
+ );
+ })
+ .calledWith(
+ 'PixTableColumn: parameters `@ariaLabelDefaultSort`, `@ariaLabelSortDesc` and `@ariaLabelSortAsc` are required for sort buttons',
+ false,
+ {
+ id: 'pix-ui.pix-table-column.sortAriaLabels.required',
+ },
+ ),
+ );
+ });
+ });
});
});
|