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"}} + +
+ {{yield to="header"}} + {{#if this.sortable}} + + {{/if}} +
{{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', + }, + ), + ); + }); + }); }); });