diff --git a/module/animate/_index.scss b/module/animate/_index.scss index 16b233c6d..8f7e32a54 100644 --- a/module/animate/_index.scss +++ b/module/animate/_index.scss @@ -1 +1,2 @@ +@forward 'mixin/rotate'; @forward 'mixin/caret'; diff --git a/module/animate/mixin/_caret.scss b/module/animate/mixin/_caret.scss index d39eae3ce..2c849b941 100644 --- a/module/animate/mixin/_caret.scss +++ b/module/animate/mixin/_caret.scss @@ -1,11 +1,5 @@ -@mixin caret() { - &::after { - transition: transform 0.3s; - } +@use 'rotate'; - &[aria-expanded='true'] { - &::after { - transform: rotate(-180deg); - } - } +@mixin caret() { + @include rotate.rotate('&[aria-expanded="true"]', '-180', 'after'); } diff --git a/module/animate/mixin/_rotate.scss b/module/animate/mixin/_rotate.scss new file mode 100644 index 000000000..1282d1879 --- /dev/null +++ b/module/animate/mixin/_rotate.scss @@ -0,0 +1,11 @@ +@mixin rotate($activeSelector, $deg: -180, $pseudo: before, $duration: 0.3) { + &::#{$pseudo} { + transition: transform #{$duration}s; + } + + #{$activeSelector} { + &::#{$pseudo} { + transform: rotate(#{$deg}deg); + } + } +} diff --git a/module/color/variable/_decisions.scss b/module/color/variable/_decisions.scss index 627d5a3ad..4b47bf75c 100644 --- a/module/color/variable/_decisions.scss +++ b/module/color/variable/_decisions.scss @@ -108,6 +108,9 @@ $values: ( primary: main, accent: main ), + contrast: ( + neutral: soft + ), active: ( primary: strong ), diff --git a/src/analytics/example/component/table/index.ejs b/src/analytics/example/component/table/index.ejs index 2bd3bf18c..cc5e5e8ad 100755 --- a/src/analytics/example/component/table/index.ejs +++ b/src/analytics/example/component/table/index.ejs @@ -1,6 +1,6 @@ -<% +<% const sample = getSample(include); - let data = { caption: 'Caption du tableau (accessibilité)', col: 6, row: 3}; + let data = { caption: 'Titre du tableau (caption)', col: 6, row: 3}; %> diff --git a/src/component/button/style/_legacy.scss b/src/component/button/style/_legacy.scss index 96d822918..d5289d70a 100644 --- a/src/component/button/style/_legacy.scss +++ b/src/component/button/style/_legacy.scss @@ -4,6 +4,7 @@ //// @use 'module/legacy'; +@use 'module/animate'; @include legacy.is(ie11) { #{ns(btn)} { @@ -68,6 +69,16 @@ @include icon-legacy(team-line, sm); } + #{ns(btn--sort)} { + @include icon-legacy(arrow-up-down-line, sm); + @include animate.rotate('&[aria-sorting='desc']'); + + &[aria-sorting='asc'], + &[aria-sorting='desc'] { + @include icon-legacy(arrow-up-line, sm); + } + } + #{ns-group(btns)} { @include disable-list-style-legacy(true); diff --git a/src/component/button/style/_module.scss b/src/component/button/style/_module.scss index 8b5cca69c..0ed843c74 100644 --- a/src/component/button/style/_module.scss +++ b/src/component/button/style/_module.scss @@ -12,3 +12,4 @@ @import 'module/account'; @import 'module/team'; @import 'module/briefcase'; +@import 'module/sort'; diff --git a/src/component/button/style/_scheme.scss b/src/component/button/style/_scheme.scss index fc077e551..23ca2a0a9 100644 --- a/src/component/button/style/_scheme.scss +++ b/src/component/button/style/_scheme.scss @@ -20,6 +20,7 @@ // Grey outline variant &--tertiary, + &--sort, &--account { @include btn-kind-scheme(3, $legacy); } diff --git a/src/component/button/style/module/_sort.scss b/src/component/button/style/module/_sort.scss new file mode 100644 index 000000000..a0febde9e --- /dev/null +++ b/src/component/button/style/module/_sort.scss @@ -0,0 +1,16 @@ +//// +/// Button Module - Expand +/// @group button +//// + +@use 'module/animate'; + +#{ns(btn--sort)} { + @include nest-btn(sm, only, arrow-up-down-line, null, false); + @include animate.rotate('&[aria-sorting='desc']'); + + &[aria-sorting='asc'], + &[aria-sorting='desc'] { + @include nest-btn(sm, only, arrow-up-line, null, false); + } +} diff --git a/src/component/checkbox/.package.yml b/src/component/checkbox/.package.yml index ebc072ffc..77b5230c2 100644 --- a/src/component/checkbox/.package.yml +++ b/src/component/checkbox/.package.yml @@ -6,3 +6,5 @@ wrapper: col-8 style: - core - form +script: + - core diff --git a/src/component/checkbox/api.js b/src/component/checkbox/api.js new file mode 100644 index 000000000..e82954c70 --- /dev/null +++ b/src/component/checkbox/api.js @@ -0,0 +1,2 @@ +import api from '../api.js'; +export default api; diff --git a/src/component/checkbox/index.js b/src/component/checkbox/index.js new file mode 100644 index 000000000..64184b81b --- /dev/null +++ b/src/component/checkbox/index.js @@ -0,0 +1,12 @@ +import api from './api.js'; +import { CheckboxSelector } from './script/checkbox/checkbox-selector'; +import { CheckboxEmission } from './script/checkbox/checkbox-emission.js'; +import { CheckboxInput } from './script/checkbox/checkbox-input.js'; + +api.checkbox = { + CheckboxSelector: CheckboxSelector, + CheckboxEmission: CheckboxEmission, + CheckboxInput: CheckboxInput +}; + +export default api; diff --git a/src/component/checkbox/main.js b/src/component/checkbox/main.js new file mode 100644 index 000000000..7bfe55e00 --- /dev/null +++ b/src/component/checkbox/main.js @@ -0,0 +1,5 @@ +import api from './index.js'; + +api.internals.register(api.checkbox.CheckboxSelector.INPUT, api.checkbox.CheckboxInput); + +export default api; diff --git a/src/component/checkbox/script/checkbox/checkbox-emission.js b/src/component/checkbox/script/checkbox/checkbox-emission.js new file mode 100644 index 000000000..fcca5eaf0 --- /dev/null +++ b/src/component/checkbox/script/checkbox/checkbox-emission.js @@ -0,0 +1,8 @@ +import api from '../../api.js'; + +const CheckboxEmission = { + CHANGE: api.internals.ns.emission('checkbox', 'change'), + RETRIEVE: api.internals.ns.emission('checkbox', 'retrieve') +}; + +export { CheckboxEmission }; diff --git a/src/component/checkbox/script/checkbox/checkbox-input.js b/src/component/checkbox/script/checkbox/checkbox-input.js new file mode 100644 index 000000000..dc7097ec2 --- /dev/null +++ b/src/component/checkbox/script/checkbox/checkbox-input.js @@ -0,0 +1,29 @@ +import api from '../../api.js'; +import { CheckboxEmission } from './checkbox-emission.js'; + +class CheckboxInput extends api.core.Instance { + static get instanceClassName () { + return 'CheckboxInput'; + } + + constructor () { + super(); + this._handlingChange = this.handleChange.bind(this); + } + + init () { + this.node.addEventListener('change', this._handlingChange); + this.addDescent(CheckboxEmission.RETRIEVE, this._handlingChange); + this.handleChange(); + } + + get isChecked () { + return this.node.checked; + } + + handleChange () { + this.ascend(CheckboxEmission.CHANGE, this.node); + } +} + +export { CheckboxInput }; diff --git a/src/component/checkbox/script/checkbox/checkbox-selector.js b/src/component/checkbox/script/checkbox/checkbox-selector.js new file mode 100644 index 000000000..362bf0bbf --- /dev/null +++ b/src/component/checkbox/script/checkbox/checkbox-selector.js @@ -0,0 +1,5 @@ +import api from '../../api.js'; + +export const CheckboxSelector = { + INPUT: `${api.internals.ns.selector('checkbox-group')} input[type="checkbox"]` +}; diff --git a/src/component/main.js b/src/component/main.js index 2c47da81e..9cb94bc5b 100644 --- a/src/component/main.js +++ b/src/component/main.js @@ -2,6 +2,7 @@ import api from './api.js'; import './accordion/main.js'; import './button/main.js'; import './card/main.js'; +import './checkbox/main.js'; import './segmented/main.js'; import './breadcrumb/main.js'; import './tooltip/main.js'; @@ -11,11 +12,11 @@ import './modal/main.js'; import './password/main.js'; import './navigation/main.js'; import './tab/main.js'; -import './table/main.js'; import './tag/main.js'; import './transcription/main.js'; import './tile/main.js'; import './range/main.js'; import './header/main.js'; import './display/main.js'; +import './table/main.js'; export default api; diff --git a/src/component/segmented/template/ejs/segmented.ejs b/src/component/segmented/template/ejs/segmented.ejs index 26a85de2c..d74c0ff98 100644 --- a/src/component/segmented/template/ejs/segmented.ejs +++ b/src/component/segmented/template/ejs/segmented.ejs @@ -42,7 +42,7 @@ switch (segmented.size) { <%= segmented.hint %> <% } %> -
+
<% for (const element of segmented.elements) { %> <%- include('./segmented-element', {element: element}); %> <% } %> diff --git a/src/component/table/.package.yml b/src/component/table/.package.yml index 8c7142e81..632ecc86b 100644 --- a/src/component/table/.package.yml +++ b/src/component/table/.package.yml @@ -7,3 +7,19 @@ style: - core script: - core + - checkbox + - tooltip +example: + style: + - alert + - badge + - button + - checkbox + - input + - link + - pagination + - select + - segmented + - tag + - toggle + - tooltip diff --git a/src/component/table/deprecated/example/index.ejs b/src/component/table/deprecated/example/index.ejs new file mode 100755 index 000000000..f713ec21f --- /dev/null +++ b/src/component/table/deprecated/example/index.ejs @@ -0,0 +1,27 @@ +<% +const sample = getSample(include); +let data = { caption: 'Titre du tableau (caption)', col: 6, row: 3}; +let dataBordered = { caption: 'Titre du tableau (caption)', bordered: true, col: 6, row: 3}; +let dataNoScroll = { caption: 'Titre du tableau (caption) non scrollable', noScroll: true, col: 6}; +let dataLayoutFixed = { caption: 'Titre du tableau (caption) fixé', layout: "fixed", col: 3}; +let dataNoCaption = { caption: 'Titre du tableau (caption) caché', noCaption: true, col: 6}; +let dataCaptionBottom = { caption: 'Titre du tableau (caption) en bas', captionBottom: true, col: 6}; + +// @TODO: revoir les grilles avec layout +%> + +<%- sample('Tableau par défaut', './sample/table-default', {table:data}, true) %> + +<%- sample('Tableau avec bordure', './sample/table-default', {table:dataBordered}, true) %> + +<%- sample('Tableau non scrollable', './sample/table-default', {table:dataNoScroll}, true) %> + +<%- sample('Tableau colonnes fixées (layout-fixed)', './sample/table-default', {table:dataLayoutFixed}, true) %> + +<%- sample('Tableau avec titre invisible', './sample/table-default', {table:dataNoCaption}, true) %> + +<%- sample('Tableau avec titre en bas', './sample/table-default', {table:dataCaptionBottom}, true) %> + +<%- sample('Tableau accentué', './sample/table-default', {table: {...data, accent:'green-emeraude'}}, true) %> + +<%- sample('Tableau accentué avec bordure', './sample/table-default', {table:{...dataBordered, accent:'green-emeraude'}}, true) %> diff --git a/src/component/table/deprecated/example/sample/table-default.ejs b/src/component/table/deprecated/example/sample/table-default.ejs new file mode 100755 index 000000000..5f220727d --- /dev/null +++ b/src/component/table/deprecated/example/sample/table-default.ejs @@ -0,0 +1,25 @@ +<% +const table = locals.table || {}; +const col = table.col || 2; +const row = table.row || 2; +let data = { + caption: table.caption || undefined, + bordered: table.bordered || false, + noScroll: table.noScroll || false, + noCaption: table.noCaption || false, + captionBottom: table.captionBottom || false, + accent: table.accent || undefined, + layout: table.layout +} + +data.data = new Array(row); +for(let r = 0; r < row; r++) { + data.data[r] = new Array(col); + for(let c = 0; c < col; c++) { + if(r === 0) data.data[0][c] = 'th' + c; + else data.data[r][c] = lorem(null , 25); + } +} +%> + +<%- include('../../template/ejs/table', {table: data}); %> diff --git a/src/component/table/deprecated/style/_legacy.scss b/src/component/table/deprecated/style/_legacy.scss new file mode 100644 index 000000000..012fd30eb --- /dev/null +++ b/src/component/table/deprecated/style/_legacy.scss @@ -0,0 +1,92 @@ +//// +/// Table Legacy - deprecated +/// @group table - deprecated +//// + +@use 'module/legacy'; +@use 'module/color'; + +@include legacy.is(ie11) { + #{ns(table)} { + & > table { + background-image: none; + + thead { + background-image: none; + + tr { + &:first-child { + th { + @include color.border(contrast grey, (legacy: true, important: false, side: 'top')); + } + } + + &:last-child { + th { + @include color.border(plain grey, (legacy: true, important: false, side: 'bottom')); + } + } + } + } + + tbody { + tr { + background-image: none; + + &:last-child { + th, + td { + @include color.border(contrast grey, (legacy: true, important: false, side: 'bottom')); + } + } + } + } + + th, + td { + &:first-child { + @include color.border(contrast grey, (legacy: true, important: false, side: 'left')); + } + + &:last-child { + @include color.border(contrast grey, (legacy: true, important: false, side: 'right')); + } + } + } + + @include color.accentuate { + & > table { + background-image: none; + + thead { + background-image: none; + } + + tbody { + tr { + background-image: none; + } + } + } + + &#{ns(table--bordered)} { + & > table { + tbody { + tr { + background-image: none; + } + } + } + } + } + + &--bordered { + & > table { + th, + td { + @include color.border(contrast grey, (legacy: true, important: false, side: 'bottom')); + } + } + } + } +} diff --git a/src/component/table/deprecated/style/_module.scss b/src/component/table/deprecated/style/_module.scss new file mode 100644 index 000000000..7507db2d4 --- /dev/null +++ b/src/component/table/deprecated/style/_module.scss @@ -0,0 +1,8 @@ +//// +/// Table Module - deprecated +/// @group Table - deprecated +//// + +@import 'module/default'; +@import 'module/variants'; +@import 'module/shadow'; diff --git a/src/component/table/deprecated/style/_scheme.scss b/src/component/table/deprecated/style/_scheme.scss new file mode 100644 index 000000000..36ef74bc3 --- /dev/null +++ b/src/component/table/deprecated/style/_scheme.scss @@ -0,0 +1,83 @@ +//// +/// Table Module - deprecated +/// @group Table - deprecated +//// + +@use 'module/color'; + +@mixin _deprecated-table-scheme($legacy: false) { + #{ns(table)} { + & > table { + @include color.background-image((border default grey) (border default grey) (border default grey) (border default grey), (legacy: $legacy)); + + caption { + @include color.text(title grey, (legacy: $legacy)); + } + + thead { + @include color.background-image(border plain grey, (legacy: $legacy)); + + @include color.background(alt grey, (legacy: $legacy)); + @include color.text(title grey, (legacy: $legacy)); + } + + tbody { + @include color.background(default grey, (legacy: $legacy)); + + tr:nth-child(even) { + @include color.background(alt grey, (legacy: $legacy)); + } + } + } + + @include color.accentuate { + & > table { + @include color.background-image((border default accent) (border default accent) (border default accent) (border default accent), (legacy: $legacy)); + + thead { + @include color.background-image(border plain accent, (legacy: $legacy)); + @include color.background(contrast accent, (legacy: $legacy)); + } + + tbody { + @include color.background(alt accent, (legacy: $legacy)); + + tr:nth-child(even) { + @include color.background(contrast accent, (legacy: $legacy)); + } + } + } + + /* Style bordered, ajoute des bordures entre chaque ligne */ + &#{ns(table--bordered)} { + & > table { + tbody { + tr { + @include color.background-image(border default accent, (legacy: $legacy)); + + &:last-child { + background-image: none; + } + } + } + } + } + } + + /* Style bordered, ajoute des bordures entre chaque ligne */ + &--bordered { + & > table { + tbody { + tr { + @include color.background-image(border default grey, (legacy: $legacy)); + + /* Style bordered, enleve le style even/odd */ + &:nth-child(even) { + @include color.transparent-background((legacy:$legacy, hover: true)); + } + } + } + } + } + } +} diff --git a/src/component/table/deprecated/style/module/_default.scss b/src/component/table/deprecated/style/module/_default.scss new file mode 100644 index 000000000..e74720388 --- /dev/null +++ b/src/component/table/deprecated/style/module/_default.scss @@ -0,0 +1,62 @@ +//// +/// Table Module : default - deprecated +/// @group Table - deprecated +//// + +#{ns(table)} { + @include before('', block) { + @include size(100%, 0); + } + + &:not(:has(#{ns(table__container)})) { + @include padding-top(var(--table-offset)); + } + + &:not(#{ns(table--no-scroll)}) { + & > table { + width: 100%; + } + } + + & > table { + @include size(100%); + display: block; + overflow: auto; + border-spacing: 0; + + @include padding(1px); + @include margin(-1px); + + background-size: 100% 1px, 1px 100%, 1px 100%, 100% 1px; + background-repeat: no-repeat, no-repeat, no-repeat; + background-position: 0 100%, 0 0, 100% 0, 100% 0; + + td, + th { + text-align: left; + vertical-align: middle; + display: table-cell; + border: 0; + @include padding(3v); + @include padding(4v, md); + @include text-style(sm); + } + + th { + font-weight: font-weight(bold); + } + + thead { + background-size: 100% 1px; + background-position: bottom; + background-repeat: no-repeat; + + td, + th { + font-weight: font-weight(bold); + @include padding-bottom(3.5v); // 0.5v for the box shadow + @include padding-bottom(4.5v, md); + } + } + } +} diff --git a/src/component/table/deprecated/style/module/_shadow.scss b/src/component/table/deprecated/style/module/_shadow.scss new file mode 100644 index 000000000..843dbcbb8 --- /dev/null +++ b/src/component/table/deprecated/style/module/_shadow.scss @@ -0,0 +1,89 @@ +//// +/// Table module : Shadow - deprecated +/// @group Table - deprecated +//// + +@use 'module/selector'; + +/* +* Ombres ajoutées en Js si le contenu est plus grand que le conteneur +*/ +#{ns(table)} { + & > table { + &#{ns(table__shadow)} { + @include before('', block) { + @include absolute(var(--table-offset), 0, 0, 0); + @include z-index(over); + @include _table-scroll-shadow(false, false); + opacity: 0.32; + pointer-events: none; + transition: box-shadow 0.3s; + } + + /** + * Modifier ombre à gauche + **/ + &--left { + @include before { + @include _table-scroll-shadow(true, false); // @TODO: à implementer dans la mixin shadow + } + + /** + * Modifier combinaison ombre à gauche et ombre à droite + **/ + &#{ns(table__shadow--right)} { + @include before('', block) { + @include _table-scroll-shadow(true, true); // @TODO: à implementer dans la mixin shadow + } + } + } + + /** + * Modifier ombre à droite + **/ + &--right { + @include before { + @include _table-scroll-shadow(false, true);// @TODO: à implementer dans la mixin shadow + } + } + } + } +} + +/* +* Positionnement ombres sur le tableau sans caption +*/ +#{ns(table--no-caption)} { + & > table { + &#{ns(table__shadow)} { + @include before('', block) { + @include absolute(0, 0, 0, 0); + } + } + } +} + +/* +* Positionnement ombres sur le tableau avec caption en bas +*/ +#{ns(table--caption-bottom)} { + & > table { + &#{ns(table__shadow)} { + @include before('', block) { + @include absolute(0, 0, 0, 0); + } + } + } +} + +@include selector.theme(dark) { + #{ns(table)} { + & > table { + &#{ns(table__shadow)} { + @include before { + opacity: 1; + } + } + } + } +} diff --git a/src/component/table/deprecated/style/module/_variants.scss b/src/component/table/deprecated/style/module/_variants.scss new file mode 100644 index 000000000..d59ee96a0 --- /dev/null +++ b/src/component/table/deprecated/style/module/_variants.scss @@ -0,0 +1,90 @@ +//// +/// Table module : Variants - deprecated +/// @group Table - deprecated +//// + +#{ns(table)} { + /* + * Cache la caption + */ + &--no-caption { + @include padding-top(0); + + & > table { + caption { + @include sr-only(); + @include height(0); + } + } + } + + /* + * Fixe le caption en bas du tableau + */ + &--caption-bottom { + @include padding-top(0); + @include margin-bottom(0); + @include margin-top(4v); + + & > table { + @include margin-bottom(calc(var(--table-offset) + 11v)); + + caption { + @include margin-top(4v); + @include height(min-content); + caption-side: bottom; + } + } + + &#{ns-attr(js-table, true)} { + & > table { + caption { + @include absolute(100%, 0, 0, 0); + @include margin-top(4v); + } + } + } + } + + /* + * pas de scroll ni de shadow + */ + &--no-scroll { + @include min-width(auto); + + & > table { + overflow-x: hidden; + + caption { + @include max-width(calc(100vw - 8v)); // eol in mobile even if table overflow + } + } + } + + /* + * Fixe la taille des colonnes du tableau + */ + &--layout-fixed { + & > table { + display: table; + table-layout: fixed; + } + } + + /* Style bordered, ajoute des bordures entre chaque ligne */ + &--bordered { + & > table { + tbody { + tr { + background-size: 100% 1px; + background-position: bottom; + background-repeat: no-repeat; + + &:last-child { + background-image: none; + } + } + } + } + } +} diff --git a/src/component/table/deprecated/template/ejs/table.ejs b/src/component/table/deprecated/template/ejs/table.ejs new file mode 100644 index 000000000..a10fb853d --- /dev/null +++ b/src/component/table/deprecated/template/ejs/table.ejs @@ -0,0 +1,72 @@ +<%# +# paramètres Table + +* table.data (array(array), required): tableau de données à deux dimension [row][col] (la première ligne (row = 0) est reservée aux ) + +* table.caption (string, required) : titre du tableau + +* table.noScroll (boolean, optional) : {default: false} désactive le scroll + +* table.noCaption (boolean, optional) : {default: false} cache le texte de la caption + +* table.captionBottom (boolean, optional) : {default: false} positionne la caption en bas + +* table.bordered (boolean, optional) : {default: false} si true, ajoute des séparateurs entre chaque ligne et enleve l'effet even/odd + +* table.layout (string, optional) : si non undefined, fix la taille des colonnes à 100%/col + +* table.col (integer, optional) : nombre de colones à afficher (si différent du nombre de colones de data) + +* table.row (integer, optional) : nombre de lignes à afficher (si différent du nombre de lignes de data) + +* table.accent (string, optional): couleur d'accenturation du composant + +* table.id (string) : id de l'élément + +* table.classes (array, optional): classes supplémentaires du composant + +* table.attributes (array, optional): attributs supplémentaires du composant +%> +<% eval(include('../../../../../core/index.ejs')); %> + +<% +let table = locals.table || {}; +let classes = table.classes || []; +const attributes = table.attributes || {}; +attributes.id = table.id || uniqueId('table'); +const data = table.data || [[]]; + +classes.push(prefix + '-table'); +if (table.accent !== undefined) classes.push(prefix + '-table--' + table.accent); +if (table.bordered) classes.push(prefix + '-table--bordered'); +if (table.noScroll) classes.push(prefix + '-table--no-scroll'); +if (table.noCaption) classes.push(prefix + '-table--no-caption'); +if (table.captionBottom) classes.push(prefix + '-table--caption-bottom'); +if (table.layout) classes.push(prefix + '-table--layout-fixed'); +if (!table.row) table.row = data.length; +if (!table.col) table.col = data[0].length; +%> + +
<%- includeAttrs(attributes) %>> + + <% if (table.caption !== undefined) { %> + + <% } %> + + + <% for (let col = 0; col < table.col; col++) { %> + + <% } %> + + + + <% for (let row = 1; row < table.row; row++) { %> + + <% for (let col = 0; col < table.col; col++) { %> + + <% } %> + + <% } %> + +
<%= table.caption %>
<%- table.data[0][col] %>
<%- table.data[row][col] %>
+
diff --git a/src/component/table/example/data/data-complex-tbody.json.ejs b/src/component/table/example/data/data-complex-tbody.json.ejs new file mode 100644 index 000000000..f6bc28d97 --- /dev/null +++ b/src/component/table/example/data/data-complex-tbody.json.ejs @@ -0,0 +1,135 @@ +<% +const getPictogram = (pictogram) => include('../../../../core/template/ejs/artwork/pictogram.ejs', {pictogram}); + +const tbody = [ + [ + { + attributes: { + headers: 'complex-row-0 complex-thead-0-col-1 complex-thead-1-col-1' + }, + content: 'Français' + }, + { + attributes: { + headers: 'complex-row-0 complex-thead-0-col-2 complex-thead-1-col-2' + }, + content: 'Mathématiques' + }, + { + attributes: { + headers: 'complex-row-0 complex-thead-0-col-3 complex-thead-1-col-3' + }, + content: 'LV1' + }, + { + attributes: { + headers: 'complex-row-0 complex-thead-0-col-3 complex-thead-1-col-4' + }, + content: 'Histoire - Géographie' + }, + { + attributes: { + headers: 'complex-row-0 complex-thead-0-col-4 complex-thead-1-col-5' + }, + content: 'EPS' + } + ], + [ + { + attributes: { + colspan: 5, + headers: 'complex-row-1 complex-thead-0-col-1 complex-thead-0-col-2 complex-thead-0-col-3 complex-thead-0-col-4 complex-thead-1-col-1 complex-thead-1-col-2 complex-thead-1-col-3 complex-thead-1-col-4 complex-thead-1-col-5' + }, + content: 'Etude dirigée
Exemple de colspan sur toute la ligne' + } + ], + [ + { + attributes: { + headers: 'complex-row-2 complex-thead-0-col-1 complex-thead-1-col-1' + }, + content: 'Mathématiques' + }, + { + attributes: { + headers: 'complex-row-2 complex-thead-0-col-2 complex-thead-1-col-2' + }, + content: 'Histoire - Géographie' + }, + { + attributes: { + rowspan: 2, + headers: 'complex-row-2 complex-row-3 complex-thead-0-col-3 complex-thead-1-col-3' + }, + content: `Arts appliqués
${getPictogram({name: 'environment'})}` + }, + { + attributes: { + headers: 'complex-row-2 complex-thead-0-col-3 complex-thead-1-col-4' + }, + content: 'LV2' + }, + { + attributes: { + headers: 'complex-row-2 complex-thead-0-col-4 complex-thead-1-col-5' + }, + content: 'Sciences' + } + ], + [ + { + attributes: { + headers: 'complex-row-3 complex-thead-0-col-1 complex-thead-1-col-1' + }, + content: 'Français' + }, + { + attributes: { + headers: 'complex-row-3 complex-thead-0-col-2 complex-thead-1-col-2' + }, + content: 'EPS' + }, + { + attributes: { + headers: 'complex-row-3 complex-thead-0-col-3 complex-thead-1-col-4' + }, + content: 'Histoire - Géographie' + }, + { + attributes: { + headers: 'complex-row-3 complex-thead-0-col-4 complex-thead-1-col-5' + }, + content: 'Physique - Chimie' + } + ], + [ + { + attributes: { + headers: 'complex-row-4 complex-thead-0-col-1 complex-thead-1-col-1' + }, + content: 'Sciences' + }, + { + attributes: { + headers: 'complex-row-4 complex-thead-0-col-2 complex-thead-1-col-2' + }, + content: 'LV1' + }, + { + attributes: { + colspan: 2, + headers: 'complex-row-4 complex-thead-0-col-3 complex-thead-1-col-3 complex-thead-1-col-4' + }, + content: 'EPS
Exemple de colspan sur 2 cellules' + }, + { + attributes: { + headers: 'complex-row-4 complex-thead-0-col-4 complex-thead-1-col-5' + }, + content: 'LV2' + } + ] +]; +%> + +<%- JSON.stringify(tbody); %> diff --git a/src/component/table/example/data/data-complex-thead.json.ejs b/src/component/table/example/data/data-complex-thead.json.ejs new file mode 100644 index 000000000..bad5dabd3 --- /dev/null +++ b/src/component/table/example/data/data-complex-thead.json.ejs @@ -0,0 +1,62 @@ +<% +const thead = [ + [ + { + attributes: { + rowspan: 2 + }, + classes: [`${prefix}-cell--fixed`], + content: `Horaires` + }, + { + content: 'Lundi' + }, + { + content: 'Mardi' + }, + { + attributes: { + colspan: 2 + }, + content: 'Mercredi & Jeudi
Exemple de 2 cellules fusionnées dans le Header' + }, + { + content: 'Vendredi' + } + ], + [ + { + attributes: { + headers: 'complex-thead-0-col-1' + }, + content: 'Groupes 1 & 2' + }, + { + attributes: { + headers: 'complex-thead-0-col-2' + }, + content: 'Groupes 1 & 2' + }, + { + attributes: { + headers: 'complex-thead-0-col-3' + }, + content: 'Groupe 1' + }, + { + attributes: { + headers: 'complex-thead-0-col-3' + }, + content: 'Groupe 2' + }, + { + attributes: { + headers: 'complex-thead-0-col-4' + }, + content: 'Groupes 1 & 2' + } + ] +]; +%> + +<%- JSON.stringify(thead); %> diff --git a/src/component/table/example/data/data-complex.json.ejs b/src/component/table/example/data/data-complex.json.ejs new file mode 100644 index 000000000..259ff7227 --- /dev/null +++ b/src/component/table/example/data/data-complex.json.ejs @@ -0,0 +1,35 @@ +<% +const thead = JSON.parse(include('./data-complex-thead.json.ejs')); +const tbodies = [JSON.parse(include('./data-complex-tbody.json.ejs'))]; + +thead.forEach((row, rowIndex) => { + row.forEach((th, thIndex) => { + th.attributes = { + ...th.attributes, + id: `complex-thead-${rowIndex}-col-${thIndex}` + }; + }); +}); + +tbodies.forEach((tbody) => { + tbody.forEach((row, index) => { + row.unshift({ + markup: 'th', + attributes: { + id: `complex-row-${index}`, + headers: 'complex-thead-0-col-0' + }, + classes: [`${prefix}-cell--fixed`], + content: `${8 + index}h` + }); + }); +}); + +const table = { + thead, + tbodies +}; + +%> + +<%- JSON.stringify(table); %> diff --git a/src/component/table/example/data/data-lorem.json.ejs b/src/component/table/example/data/data-lorem.json.ejs new file mode 100644 index 000000000..faa3cb2cb --- /dev/null +++ b/src/component/table/example/data/data-lorem.json.ejs @@ -0,0 +1,24 @@ +<% +const table = locals.table || {}; +const col = table.col || 4; +const row = table.row || 3; +const loremLength = table.loremLength || 25; + +let thead = Array.from(Array(1), () => []); +let tbody = Array.from(Array(row), () => []); + +for(let r = 0; r < row; r++) { + for(let c = 0; c < col; c++) { + if(r === 0) thead[0].push({content: 'th' + c}); + tbody[r].push({content: lorem(null , loremLength)}); + } +} + +const data = { + ...table, + thead, + tbodies: [tbody] +}; +%> + +<%- JSON.stringify(data); %> diff --git a/src/component/table/example/data/data-miscellaneous.json.ejs b/src/component/table/example/data/data-miscellaneous.json.ejs new file mode 100644 index 000000000..2a5a8b351 --- /dev/null +++ b/src/component/table/example/data/data-miscellaneous.json.ejs @@ -0,0 +1,118 @@ +<% +const table = locals.table || {}; + +const getSrOnlyCell = (content) => `${content}`; +const getTextCell = (markup = 'span', classes = `${prefix}-cell__desc`) => `<${markup} class="${classes}">${getText('data.cell.text', 'table')}`; +const getTitleCell = (markup = 'span', classes = `${prefix}-cell__title`) => `<${markup} class="${classes}">${getText('data.cell.title', 'table')}`; + +const getBadge = (badge) => include('../../../badge/template/ejs/badge.ejs', {badge}); +const getButton = (button) => include('../partials/getButton.ejs', {button}); +const getCheckbox = (checkbox) => include('../partials/getCheckbox.ejs', {checkbox}); +const getIcon = (icon) => include('../../../../core/template/ejs/icon/icon.ejs', {icon}); +const getInput = (input) => include('../../../input/input-base/template/ejs/input-group.ejs', {input}); +const getLink = (link) => include('../../../link/template/ejs/link.ejs', {link}); +const getPictogram = (pictogram) => include('../../../../core/template/ejs/artwork/pictogram.ejs', {pictogram}); +const getTag = (tag) => include('../../../tag/template/ejs/tag.ejs', {tag}); + +const thead = [ + [ + { + attributes: { + role: 'columnheader' + }, + classes: [`${prefix}-cell--fixed`], + content: getSrOnlyCell(getText('data.cell.action.select', 'table')) + }, + { + content: getTitleCell() + }, + { + content: getTitleCell('div') + getTextCell('div') + }, + { + content: `
${getTitleCell()} ${getButton({id: `${table.id}-thead-sort-asc-desc`, label: `${getText('data.cell.action.sort', 'table')}`, classes: [`${prefix}-btn--sort`]})}
` + }, + { + content: getButton({id: `${table.id}-thead-sort-desc`, label: `${getText('data.cell.action.sort', 'table')}`, classes: [`${prefix}-btn--sort`], attributes: {'aria-sorting': 'desc'}}) + }, + { + content: `${getTextCell()} ${getButton({label: getText('data.cell.text', 'table'), classes: [`${prefix}-ml-2v`, `${prefix}-btn--tooltip`], tooltip: {id: `${table.id}-thead-tooltip`, content: lorem()}})}` + }, + { + content: getButton({id: `${table.id}-thead-sort-asc`, label: `${getText('data.cell.action.sort', 'table')}`, classes: [`${prefix}-btn--sort`], attributes: {'aria-sorting': 'asc'}}) + }, + { + content: getBadge({id: `${table.id}-thead-badge`, type: 'info', label: getText('data.cell.label', 'table')}) + }, + { + content: getTextCell() + }, + { + content: getTitleCell() + }, + { + content: getTitleCell() + getTextCell('span', `${prefix}-cell__desc ${prefix}-ml-2v`) + }, + { + content: getIcon({name: 'arrow-right-s-line', size: 'sm', classes: [`${prefix}-mr-2v`]}) + getText('data.cell.text', 'table') + } + ] +]; + +const rowsLength = 6; +const selectedLines = [0, 3, 4]; +const tbody = new Array(rowsLength); +for(let r = 0; r < rowsLength; r++) { + tbody[r] = [ + { + markup: 'th', + attributes: {scope: 'row'}, + classes: [`${prefix}-cell--fixed`], + content: getCheckbox({id: `${table.id}-select-row-checkbox-${r + 1}`, label: `${getText('data.cell.action.selectRow', 'table')} ${r + 1} : ${getText('data.cell.title', 'table')}`, attributes: {name: 'row-select', checked: selectedLines.includes(r) || undefined}}) + getTitleCell('span') + }, + { + content: getText('data.cell.text', 'table') + }, + { + content: getBadge({type: 'info', label: getText('data.cell.label', 'table'), classes: [`${prefix}-mb-2v`], size: 'sm'}) + getTitleCell('div', `${prefix}-cell__title ${prefix}-mb-2v`) + getTextCell('div') + }, + { + content: getBadge({type: 'success', label: getText('data.cell.label', 'table'), classes: [`${prefix}-mr-2v`], size: 'sm'}) + getText('data.cell.text', 'table') + }, + { + content: getText('data.cell.text', 'table') + }, + { + content: getInput({id: `text-input-text-${r + 1}`}) + }, + { + content: getText('data.cell.number', 'table') + }, + { + content: getBadge({type: 'info', label: getText('data.cell.label', 'table')}) + }, + { + content: getButton({kind: 2}) + }, + { + content: getTag({type: 'clickable', href: '#', label: getText('data.cell.label', 'table')}) + }, + { + content: getLink({download: true, href: '/example/img/image.jpg', label: getText('data.cell.download.label', 'table'), detail: getText('data.cell.download.detail', 'table')}) + }, + { + content: getPictogram({name: 'leaf'}) + } + ]; +} + +const data = { + table: { + ...table, + thead, + tbodies: [tbody] + } +}; +%> + +<%- JSON.stringify(data); %> diff --git a/src/component/table/example/data/data-simple.json.ejs b/src/component/table/example/data/data-simple.json.ejs new file mode 100644 index 000000000..5361de5aa --- /dev/null +++ b/src/component/table/example/data/data-simple.json.ejs @@ -0,0 +1,72 @@ +<% +const table = locals.table || {}; +const dataLorem = JSON.parse(include('../data/data-lorem.json.ejs', { table: table.table })); + +const thead = dataLorem.thead; +const tbodies = dataLorem.tbodies; + +const getCheckbox = (checkbox) => include('../partials/getCheckbox.ejs', {checkbox}); +const getSrOnlyCell = (content) => `${content}`; + +thead.forEach((row, rowIndex) => { + row.forEach((th, thIndex) => { + th.attributes = { + ...th.attributes, + scope: 'col' + }; + }); +}); + +if (table.selectable) { + thead.forEach((row) => { + row.unshift({ + attributes: { + role: 'columnheader' + }, + classes: [`${prefix}-cell--fixed`], + content: getSrOnlyCell(getText('data.cell.action.select', 'table')) + }); + }); + tbodies.forEach((tbody) => { + tbody.forEach((row, index) => { + row.unshift({ + classes: [`${prefix}-cell--fixed`], + content: getCheckbox({id: `${uniqueId('table-select-checkbox')}--${index}`, label: `${getText('data.cell.action.selectRow', 'table')} ${index + 1}`, attributes: {name: 'row-select'}}) + }); + }); + }); +} + +if (table.doubleEntry) { + thead.forEach((row) => { + row.unshift({ + attributes: { + scope: 'row' + }, + classes: [`${prefix}-cell--fixed`], + content: getSrOnlyCell(getText('data.cell.doubleEntry', 'table')) + }); + }); + + tbodies.forEach((tbody) => { + tbody.forEach((row, index) => { + row.unshift({ + markup: 'th', + attributes: {scope: 'row'}, + classes: [`${prefix}-cell--fixed`], + content: `th${index}` + }); + }); + }); +} + +const data = { + table: { + ...table, + thead, + tbodies + } +}; +%> + +<%- JSON.stringify(data); %> diff --git a/src/component/table/example/deprecated/index.ejs b/src/component/table/example/deprecated/index.ejs new file mode 100644 index 000000000..bcf14b10c --- /dev/null +++ b/src/component/table/example/deprecated/index.ejs @@ -0,0 +1,3 @@ +<%- deprecation(); %> + +<%- include('../../deprecated/example/index') %> diff --git a/src/component/table/example/footer/footer.ejs b/src/component/table/example/footer/footer.ejs new file mode 100644 index 000000000..8f9212ca9 --- /dev/null +++ b/src/component/table/example/footer/footer.ejs @@ -0,0 +1,79 @@ +<% eval(include('../../../../core/index.ejs')); + +const selectData = { + id: uniqueId('table-footer-select'), + label: getText('data.navigation.select.label', 'table'), + labeClasses: [`${prefix}-sr-only`], + placeholder: getText('data.navigation.select.placeholder', 'table'), + options :[ + {value:'4', label:getText('data.navigation.select.options.option1', 'table')}, + {value:'10', label:getText('data.navigation.select.options.option2', 'table')}, + {value:'20', label:getText('data.navigation.select.options.option3', 'table')}, + ] +} +const select = include('../../../select/template/ejs/select-group.ejs', {select: selectData}); + + +const getData = (label, displayedLg = false, hasLgLabel = false, title = null) => { + return { + id: uniqueId('table-footer-pagination'), + label: label, + title: title, + href: '#', + displayedLg: displayedLg === true, + hasLgLabel: hasLgLabel === true + }; +}; + +const getPageData = (pageNumber, displayedLg) => { + return getData(pageNumber, displayedLg === true, false, `Page ${pageNumber}`); +} + +const getNavData = (label, hasLgLabel) => { + return getData(label, false, hasLgLabel === true, label); +} + +const pages = []; +let i; +for (i = 1; i < 4; i++) pages.push(getPageData(i, i > 2)); +pages[0].active = true; + +const paginationData = { + first: getNavData('Première page'), + prev: getNavData('Précédente', true), + next: getNavData('Suivante', true), + last: getNavData('Dernière page'), + pages : pages +} +const pagination = include('../../../pagination/template/ejs/pagination', {pagination: paginationData}); + +const buttonsGroupData = { + inline: 'md', + align:'right', + reverse: true, + buttons: [ + { + label: getText('data.navigation.buttons.label', 'table'), + id: uniqueId('table-footer-button-primary'), + kind: 1 + }, + { + label: getText('data.navigation.buttons.label', 'table'), + id: uniqueId('table-footer-button-secondary'), + kind: 2 + } + ] +}; +const buttons = include('../../../button/template/ejs/buttons-group', {buttonsGroup: buttonsGroupData}); +%> + + + + diff --git a/src/component/table/example/header/header.ejs b/src/component/table/example/header/header.ejs new file mode 100644 index 000000000..f82602e0b --- /dev/null +++ b/src/component/table/example/header/header.ejs @@ -0,0 +1,54 @@ +<% eval(include('../../../../core/index.ejs')); + +const buttonsGroupData = { + inline: 'md', + align:'right', + iconPlace:'left', + buttons: [ + { + label: getText('data.actionBar.buttons.label', 'table'), + id: uniqueId('table-header-button-primary'), + icon: 'settings-5-line', + iconPlace:'left', + kind: 2 + }, + { + label: getText('data.actionBar.buttons.label', 'table'), + id: uniqueId('table-header-button-secondary'), + icon: 'settings-5-line', + iconPlace:'left', + kind: 2 + } + ] +}; +const buttons = include('../../../button/template/ejs/buttons-group', {buttonsGroup: buttonsGroupData}); + +const segmentedData = { + id: uniqueId('table-header-segmented'), + noLegend: true, + legend: getText('data.actionBar.segmented.legend', 'table'), + elements: [ + { + id: uniqueId('table-header-segmented-table'), + label: getText('data.actionBar.segmented.table', 'table'), + name: 'table-header-segmented-table', + value: 1, + icon: 'table-line', + checked: true + }, + { + id: uniqueId('table-header-segmented-list'), + label: getText('data.actionBar.segmented.list', 'table'), + name: 'table-header-segmented-table', + value: 2, + icon: 'list-unordered', + checked: false + } + ] +}; +const segmented = include('../../../segmented/template/ejs/segmented', {segmented: segmentedData}); +%> + +<%- segmented %> +

<%- getText('data.actionBar.detail', 'table') %>

+<%- buttons %> diff --git a/src/component/table/example/index.ejs b/src/component/table/example/index.ejs index 006b5cd68..530998151 100755 --- a/src/component/table/example/index.ejs +++ b/src/component/table/example/index.ejs @@ -1,27 +1,58 @@ + <% const sample = getSample(include); -let data = { caption: 'Caption du tableau (accessibilité)', col: 6, row: 3}; -let dataBordered = { caption: 'Caption du tableau (accessibilité)', bordered: true, col: 6, row: 3}; -let dataNoScroll = { caption: 'Caption tableau non scrollable', noScroll: true, col: 6}; -let dataLayoutFixed = { caption: 'Caption tableau fixé', layout: "fixed", col: 3}; -let dataNoCaption = { caption: 'Caption cachée', noCaption: true, col: 6}; -let dataCaptionBottom = { caption: 'Caption en bas', captionBottom: true, col: 6}; +let loremData = JSON.parse(include('./data/data-lorem.json.ejs')); +const complexData = JSON.parse(include('./data/data-complex.json.ejs')); +%> + +<%- + sample(getText('sample.default', 'table'), './sample/table-simple', {table: {table: {id: 'table-md', caption: getText('data.caption.default', 'table')}}}, true); +%> + +<%- + sample(getText('sample.sm', 'table'), './sample/table-simple', {table: {classes: [`${prefix}-table--sm`], table: {id: 'table-sm', caption: getText('data.caption.default', 'table')}}}, true); +%> + +<%- + sample(getText('sample.lg', 'table'), './sample/table-simple', {table: {classes: [`${prefix}-table--lg`], table: {id: 'table-lg', caption: getText('data.caption.default', 'table')}}}, true); +%> + +<%- + sample(getText('sample.bordered', 'table'), './sample/table-simple', {table: {bordered: true, table: {id: 'table-bordered', caption: getText('data.caption.default', 'table')}}}, true); +%> -// @TODO: revoir les grilles avec layout +<%- + sample({title: getText('sample.columnSizes.title', 'table'), subtitle: getText('sample.columnSizes.subtitle', 'table')}, './sample/table-column-sizes', {table: {table: {id: 'table-column-sizes', caption: getText('data.caption.default', 'table')}}}, true); %> -<%- sample('Tableau par défaut', './sample/table-default', {table:data}, true) %> +<%- + sample({title: getText('sample.utilityClasses.title', 'table'), subtitle: getText('sample.utilityClasses.subtitle', 'table')}, './sample/table-utility-classes', {table: {bordered: true, table: {id: 'table-default', caption: getText('data.caption.default', 'table')}}}, true); +%> -<%- sample('Tableau avec bordure', './sample/table-default', {table:dataBordered}, true) %> +<%- + sample({title: getText('sample.multiline.title', 'table'), subtitle: getText('sample.multiline.subtitle', 'table')}, './sample/table-simple', {table: {table: {id: 'table-multiline', classes: [`${prefix}-cell--multiline`], caption: getText('data.caption.default', 'table')}}}, true); +%> -<%- sample('Tableau non scrollable', './sample/table-default', {table:dataNoScroll}, true) %> +<%- + sample(getText('sample.noCaption', 'table'), './sample/table-simple', {table: {noCaption: true, table: {id: 'table-noCaption', caption: getText('data.caption.default', 'table')}}}, true); +%> -<%- sample('Tableau colonnes fixées (layout-fixed)', './sample/table-default', {table:dataLayoutFixed}, true) %> +<%- + sample(getText('sample.captionBottom', 'table'), './sample/table-simple', {table: {captionBottom: true, table: {id: 'table-captionBottom', caption: getText('data.caption.default', 'table')}}}, true); +%> -<%- sample('Tableau caption invisible', './sample/table-default', {table:dataNoCaption}, true) %> +<%- + sample(getText('sample.selectable', 'table'), './sample/table-simple', {table: {table: {id: 'table-selectable', caption: getText('data.caption.default', 'table'), selectable: true}}}, true); +%> -<%- sample('Tableau caption Bottom', './sample/table-default', {table:dataCaptionBottom}, true) %> +<%- + sample(getText('sample.doubleEntry', 'table'), './sample/table-simple', {table: {table: {id: 'table-double-entry', caption: getText('data.caption.default', 'table'), doubleEntry: true}}}, true); +%> -<%- sample('Tableau accentué', './sample/table-default', {table: {...data, accent:'green-emeraude'}}, true) %> +<%- + sample({title: getText('sample.complex.title', 'table'), subtitle: getText('sample.complex.subtitle', 'table')}, './sample/table-default', {table: {bordered: true, table: {id: 'table-complex', caption: getText('data.caption.default', 'table'), captionDetail: getText('data.caption.detail', 'table'), ...complexData}}}, true); +%> -<%- sample('Tableau accentué avec bordure', './sample/table-default', {table:{...dataBordered, accent:'green-emeraude'}}, true) %> +<%- + sample(getText('sample.miscellaneous', 'table'), './sample/table-miscellaneous', {table: {header: true, footer: true, table: {id: 'table-miscellaneous', caption: getText('data.caption.large', 'table')}}}, true); +%> diff --git a/src/component/table/example/partials/getButton.ejs b/src/component/table/example/partials/getButton.ejs new file mode 100644 index 000000000..4415e57f0 --- /dev/null +++ b/src/component/table/example/partials/getButton.ejs @@ -0,0 +1,17 @@ +<% +const button = locals.button || {}; +const defaultButton = { + id: uniqueId('table-default-button'), + label: getText('data.cell.action.button', 'table'), + size: 'sm' +}; + +const option = { + ...defaultButton, + ...button +}; + +const getButton = include('../../../button/template/ejs/button.ejs', {button: option}); +%> + +<%- getButton %> diff --git a/src/component/table/example/partials/getCheckbox.ejs b/src/component/table/example/partials/getCheckbox.ejs new file mode 100644 index 000000000..ca8bf3eb0 --- /dev/null +++ b/src/component/table/example/partials/getCheckbox.ejs @@ -0,0 +1,20 @@ +<% +const checkbox = locals.checkbox || {} +const defaultCheckbox = { + id: uniqueId('table-default-checkbox'), + attributes: { + 'aria-label': getText('data.cell.action.select', 'table'), + }, + size: 'sm', + includeEmptyMessagesGroup: false, +}; + +const option = { + ...defaultCheckbox, + ...checkbox +}; + +const getCheckbox = include('../../../checkbox/template/ejs/checkbox-group.ejs', {checkbox: option}); +%> + +<%- getCheckbox %> diff --git a/src/component/table/example/sample/table-column-sizes.ejs b/src/component/table/example/sample/table-column-sizes.ejs new file mode 100644 index 000000000..78c138096 --- /dev/null +++ b/src/component/table/example/sample/table-column-sizes.ejs @@ -0,0 +1,29 @@ +<% +const table = locals.table || {}; +const dataLorem = JSON.parse(include('../data/data-lorem.json.ejs', { table: table.table })); + +const loremThead = dataLorem.thead; +loremThead.forEach(thead => { + thead.forEach((row, index) => { + const sizes = ['xs','sm', 'md', 'lg']; + row.content = sizes[index] || 'default'; + row.classes = sizes[index] ? [`${prefix}-col--${sizes[index]}`] : []; + }); +}); + +const loremBodies = dataLorem.tbodies; +loremBodies.forEach(tbodies => { + tbodies.forEach(tbody => { + tbody.forEach((row, index) => { + row.content = lorem(null , 5 * (index + 1)); + }); + }); +}); + +const data = { + ...table, + table: dataLorem +} +%> + +<%- include('./table-default', {table: data}); %> diff --git a/src/component/table/example/sample/table-default.ejs b/src/component/table/example/sample/table-default.ejs old mode 100755 new mode 100644 index 5f220727d..ffb11523c --- a/src/component/table/example/sample/table-default.ejs +++ b/src/component/table/example/sample/table-default.ejs @@ -1,25 +1,13 @@ <% const table = locals.table || {}; -const col = table.col || 2; -const row = table.row || 2; -let data = { - caption: table.caption || undefined, - bordered: table.bordered || false, - noScroll: table.noScroll || false, - noCaption: table.noCaption || false, - captionBottom: table.captionBottom || false, - accent: table.accent || undefined, - layout: table.layout + +if (table.header) { + table.header = include('../header/header.ejs'); } -data.data = new Array(row); -for(let r = 0; r < row; r++) { - data.data[r] = new Array(col); - for(let c = 0; c < col; c++) { - if(r === 0) data.data[0][c] = 'th' + c; - else data.data[r][c] = lorem(null , 25); - } +if (table.footer) { + table.footer = include('../footer/footer.ejs'); } %> -<%- include('../../template/ejs/table', {table: data}); %> +<%- include('../../template/ejs/wrapper', {table}); %> diff --git a/src/component/table/example/sample/table-miscellaneous.ejs b/src/component/table/example/sample/table-miscellaneous.ejs new file mode 100644 index 000000000..00a056e06 --- /dev/null +++ b/src/component/table/example/sample/table-miscellaneous.ejs @@ -0,0 +1,11 @@ +<% +const table = locals.table || {}; +const dataMiscellaneous = JSON.parse(include('../data/data-miscellaneous.json.ejs', { table: table.table })); + +const data = { + ...table, + ...dataMiscellaneous +} +%> + +<%- include('./table-default', {table: data}); %> diff --git a/src/component/table/example/sample/table-simple.ejs b/src/component/table/example/sample/table-simple.ejs new file mode 100644 index 000000000..a0f71e2ab --- /dev/null +++ b/src/component/table/example/sample/table-simple.ejs @@ -0,0 +1,11 @@ +<% +const table = locals.table || {}; +const dataSimple = JSON.parse(include('../data/data-simple.json.ejs', { table: table.table })); + +const data = { + ...table, + ...dataSimple +} +%> + +<%- include('./table-default', {table: data}); %> diff --git a/src/component/table/example/sample/table-utility-classes.ejs b/src/component/table/example/sample/table-utility-classes.ejs new file mode 100644 index 000000000..ee9b76fae --- /dev/null +++ b/src/component/table/example/sample/table-utility-classes.ejs @@ -0,0 +1,32 @@ +<% +const table = locals.table || {}; +const dataLorem = JSON.parse(include('../data/data-lorem.json.ejs', { table: table.table })); + +const utilities = ['top', 'bottom', 'center', 'right']; + +const loremThead = dataLorem.thead; +loremThead.forEach(thead => { + thead.forEach((row, index) => { + row.content = utilities[index] || 'default'; + }); +}); + +const loremBodies = dataLorem.tbodies; +loremBodies.forEach(tbodies => { + tbodies.forEach(tbody => { + tbody.forEach((row, index) => { + row.classes = utilities[index] ? [`${prefix}-cell--${utilities[index]}`] : []; + if (index === tbody.length - 1) { + row.content = `${lorem(null , 5)}
${lorem(null , 10)}
${lorem(null , 25)}` + } + }); + }); +}); + +const data = { + ...table, + table: dataLorem +} +%> + +<%- include('./table-default', {table: data}); %> diff --git a/src/component/table/i18n/fr.yml b/src/component/table/i18n/fr.yml new file mode 100644 index 000000000..656cb809e --- /dev/null +++ b/src/component/table/i18n/fr.yml @@ -0,0 +1,71 @@ +title: Tableaux +description: Les tableaux sont utilisés pour organiser et afficher les informations d'un jeu de données. + +subdir: + title: Autres versions + deprecated: version dépréciée + +sample: + bordered: Tableau avec bordures verticales + captionBottom: Tableau avec titre en bas + columnSizes: + subtitle: En ajoutant les classes fr-col--xs, fr-col--sm, fr-col--md ou fr-col--lg sur les cellules d'en-tête du tableau + title: Exemple des colonnes de tailles minimales fixées + complex: + subtitle: Lorsqu’un tableau de données contient des en-têtes qui ne sont pas répartis uniquement sur la première ligne et/ou la première colonne de la grille ou dont la portée n’est pas valable pour l’ensemble de la colonne ou de la ligne, on parle de tableau de données complexe. Il est alors nécessaire de fournir un « résumé » permettant d’en expliquer sa nature et sa structure afin d’en faciliter la consultation pour des utilisateurs de technologies d’assistance par exemple. + title: Tableau complexe (comportant des cellules fusionnées) + default: Tableau MD (défaut) + doubleEntry: Tableau à double entrée + lg: Tableau LG + miscellaneous: Tableau avec filtre et navigation comportant différents types de données + multiline: + subtitle: Il est possible de réactiver le retour à la ligne des éléments inline en ajoutant la classe fr-cell--multiline sur l'element table, une ligne ou une cellule du tableau + title: Exemple de cellules avec retour à la ligne automatique + noCaption: Tableau avec titre invisible + selectable: Tableau avec lignes sélectionnables + sm: Tableau SM + utilityClasses: + subtitle: Par défaut le contenu des cellules est centré verticalement et aligné à gauche mais peut être modifié à l'aide des classe utilitaires fr-cell--top, fr-cell--bottom pour l'alignement vertical et fr-cell--center, fr-cell--right pour l'alignement horizontal + title: Exemple des classes utilitaires d'alignement + +data: + caption: + detail: '(Résumé) Emploi du temps horaire des Groupes 1 et 2, le matin des jours de la semaine ouvrée (Lundi au Vendredi) : ' + default: Titre du tableau (caption) + large: Titre long du tableau (caption) Repellat natus illo omnis nulla nostrum ut doloremque ipsam voluptas officiis cumque porro. + cell: + action: + button: Libellé bouton + empty: Action + sort: Trier + select: Sélectionner + selectAll: Sélectionner toutes les lignes + selectRow: Sélectionner la ligne + collapsed: Exemple de cellule fusionnée + doubleEntry: En tête de colonne [À MODIFIER] + download: + detail: JPG – 61,88 Ko + label: Lien de Téléchargement + label: Libellé par défaut + number: 30,00 € + text: Texte par défaut + title: Titre par défaut + navigation: + buttons: + label: Action tableau + detail: 215 lignes + select: + label: Nombre de lignes par page + placeholder: Nombre de lignes par page + options: + option1: 4 lignes par page + option2: 10 lignes par page + option3: 20 lignes par page + actionBar: + buttons: + label: Action groupée + detail: Nombre de lignes sélectionnées + segmented: + legend: Type d'affichage + list: Liste + table: Tableau diff --git a/src/component/table/index.js b/src/component/table/index.js index cbf23d3c4..073404a87 100644 --- a/src/component/table/index.js +++ b/src/component/table/index.js @@ -3,12 +3,14 @@ import { Table } from './script/table/table.js'; import { TableElement } from './script/table/table-element.js'; import { TableSelector } from './script/table/table-selector.js'; import { TableCaption } from './script/table/table-caption.js'; +import { TableRow } from './script/table/table-row.js'; api.table = { Table: Table, TableElement: TableElement, TableCaption: TableCaption, - TableSelector: TableSelector + TableSelector: TableSelector, + TableRow: TableRow }; export default api; diff --git a/src/component/table/legacy.scss b/src/component/table/legacy.scss index d421e53a3..52928c089 100644 --- a/src/component/table/legacy.scss +++ b/src/component/table/legacy.scss @@ -12,5 +12,10 @@ @import 'index'; @import 'style/scheme'; @import 'style/legacy'; +@import 'deprecated/style/scheme'; @include _table-scheme(true); +@include _deprecated-table-scheme(true); + +// deprecated +@import 'deprecated/style/legacy'; diff --git a/src/component/table/main.js b/src/component/table/main.js index 5205366aa..4fc3de0fa 100644 --- a/src/component/table/main.js +++ b/src/component/table/main.js @@ -3,5 +3,6 @@ import api from './index.js'; api.internals.register(api.table.TableSelector.TABLE, api.table.Table); api.internals.register(api.table.TableSelector.ELEMENT, api.table.TableElement); api.internals.register(api.table.TableSelector.CAPTION, api.table.TableCaption); +api.internals.register(api.table.TableSelector.ROW, api.table.TableRow); export default api; diff --git a/src/component/table/main.scss b/src/component/table/main.scss index 9f4572c8c..9c1541659 100644 --- a/src/component/table/main.scss +++ b/src/component/table/main.scss @@ -18,3 +18,9 @@ @import 'style/scheme'; @include _table-scheme; + +// deprecated +@import 'deprecated/style/module'; +@import 'deprecated/style/scheme'; + +@include _deprecated-table-scheme; diff --git a/src/component/table/script/table/table-element.js b/src/component/table/script/table/table-element.js index 083af8785..f82701565 100644 --- a/src/component/table/script/table/table-element.js +++ b/src/component/table/script/table/table-element.js @@ -1,7 +1,7 @@ import api from '../../api.js'; import { TableSelector } from './table-selector.js'; -const SCROLL_OFFSET = 8; // valeur en px du scroll avant laquelle le shadow s'active ou se desactive +const SCROLL_OFFSET = 0; // valeur en px du scroll avant laquelle le shadow s'active ou se desactive class TableElement extends api.core.Instance { static get instanceClassName () { @@ -11,6 +11,7 @@ class TableElement extends api.core.Instance { init () { this.listen('scroll', this.scroll.bind(this)); this.content = this.querySelector('tbody'); + this.tableOffsetHeight = 0; this.isResizing = true; } diff --git a/src/component/table/script/table/table-emission.js b/src/component/table/script/table/table-emission.js index 5eb292441..ca3bec541 100644 --- a/src/component/table/script/table/table-emission.js +++ b/src/component/table/script/table/table-emission.js @@ -3,5 +3,6 @@ import api from '../../api.js'; export const TableEmission = { SCROLLABLE: api.internals.ns.emission('table', 'scrollable'), CHANGE: api.internals.ns.emission('table', 'change'), - CAPTION_HEIGHT: api.internals.ns.emission('table', 'captionheight') + CAPTION_HEIGHT: api.internals.ns.emission('table', 'captionheight'), + CAPTION_WIDTH: api.internals.ns.emission('table', 'captionwidth') }; diff --git a/src/component/table/script/table/table-row.js b/src/component/table/script/table/table-row.js new file mode 100644 index 000000000..3c252a27d --- /dev/null +++ b/src/component/table/script/table/table-row.js @@ -0,0 +1,41 @@ +import api from '../../api.js'; +import { CheckboxEmission } from '../../../checkbox/script/checkbox/checkbox-emission.js'; + +class TableRow extends api.core.Instance { + static get instanceClassName () { + return 'TableRow'; + } + + init () { + if (api.checkbox) { + this.addAscent(CheckboxEmission.CHANGE, this._handleCheckboxChange.bind(this)); + this.descend(CheckboxEmission.RETRIEVE); + } + } + + _handleCheckboxChange (node) { + if (node.name === 'row-select') { + this.isSelected = node.checked === true; + } + } + + render () { + const height = this.getRect().height + 2; + if (this._height === height) return; + this._height = height; + this.setProperty('--row-height', `${this._height}px`); + } + + get isSelected () { + return this._isSelected; + } + + set isSelected (value) { + if (this._isSelected === value) return; + this.isRendering = value; + this._isSelected = value; + this.setAttribute('aria-selected', value); + } +} + +export { TableRow }; diff --git a/src/component/table/script/table/table-selector.js b/src/component/table/script/table/table-selector.js index 3d7d187ba..d63ef9948 100644 --- a/src/component/table/script/table/table-selector.js +++ b/src/component/table/script/table/table-selector.js @@ -5,6 +5,8 @@ export const TableSelector = { SHADOW: api.internals.ns.selector('table__shadow'), SHADOW_LEFT: api.internals.ns.selector('table__shadow--left'), SHADOW_RIGHT: api.internals.ns.selector('table__shadow--right'), - ELEMENT: `${api.internals.ns.selector('table')}:not(${api.internals.ns.selector('table--no-scroll')}) table`, - CAPTION: `${api.internals.ns.selector('table')} table caption` + ELEMENT: [`${api.internals.ns.selector('table')}:not(${api.internals.ns.selector('table--no-scroll')}) table`, `${api.internals.ns.selector('table')} ${api.internals.ns.selector('table__container')}`], + CAPTION: `${api.internals.ns.selector('table')} table caption`, + ROW: `${api.internals.ns.selector('table')} tbody tr`, + COL: `${api.internals.ns.selector('table')} thead th` }; diff --git a/src/component/table/script/table/table.js b/src/component/table/script/table/table.js index f56223a5a..2eb877868 100644 --- a/src/component/table/script/table/table.js +++ b/src/component/table/script/table/table.js @@ -9,6 +9,7 @@ class Table extends api.core.Instance { } init () { + this.rowsHeaderWidth = []; this.addAscent(TableEmission.CAPTION_HEIGHT, this.setCaptionHeight.bind(this)); } diff --git a/src/component/table/style/_legacy.scss b/src/component/table/style/_legacy.scss index 2138a7e7a..6c5c860fc 100644 --- a/src/component/table/style/_legacy.scss +++ b/src/component/table/style/_legacy.scss @@ -4,15 +4,83 @@ //// @use 'module/legacy'; +@use 'module/spacing'; @include legacy.is(ie11) { - /** - * Correctif placement caption - */ #{ns(table)} { + &__header { + #{ns(segmented)} { + flex: 1; + } + + #{ns(table__detail)} { + flex: 2; + } + } + + &__wrapper { + @include before(none); + } + + &__content { + table { + border-top: 1px solid; + border-left: 1px solid; + border-right: 1px solid; + + #{ns(cell--fixed)} { + @include relative; + + #{ns(checkbox-group)} { + #{ns(label)} { + @include before(none); + } + + input[type="checkbox"] { + top: spacing.space(2v); + } + } + } + + tbody { + tr { + &[aria-selected=true] { + @include after(none); + + th, + td { + border-top: 2px solid; + border-bottom: 2px solid; + + &:first-child { + border-left: 2px solid; + } + + &:last-child { + border-right: 2px solid; + } + } + } + } + } + } + } + &#{ns-attr(js-table, true)} { + &#{ns(table--no-caption)}, + &#{ns(table--caption-bottom)} { + #{ns(table__wrapper)} { + @include before(none); + } + + caption { + @include relative; + } + } + caption { @include relative; + @include margin-bottom(4v); } } } diff --git a/src/component/table/style/_module.scss b/src/component/table/style/_module.scss index cd0fac194..5f0883948 100644 --- a/src/component/table/style/_module.scss +++ b/src/component/table/style/_module.scss @@ -4,5 +4,9 @@ //// @import 'module/default'; -@import 'module/variants'; +@import 'module/header'; +@import 'module/footer'; +@import 'module/sm'; +@import 'module/lg'; @import 'module/shadow'; +@import 'module/variants'; diff --git a/src/component/table/style/_scheme.scss b/src/component/table/style/_scheme.scss index 208946d67..5c9513910 100644 --- a/src/component/table/style/_scheme.scss +++ b/src/component/table/style/_scheme.scss @@ -7,61 +7,69 @@ @mixin _table-scheme($legacy: false) { #{ns(table)} { - caption { - @include color.text(title grey, (legacy: $legacy)); - } - - thead { - @include color.background-image(border plain grey, (legacy: $legacy)); - - @include color.background(contrast grey, (legacy: $legacy)); - @include color.text(title grey, (legacy: $legacy)); - } - - tbody { - @include color.background(alt grey, (legacy: $legacy)); - - tr:nth-child(even) { - @include color.background(contrast grey, (legacy: $legacy)); + &__wrapper { + @include before { + @include color.box-shadow(contrast grey, (legacy:$legacy), top-1-in left-1-in bottom-1-in right-1-in); } } - @include color.accentuate { - thead { - @include color.background-image(border plain accent, (legacy: $legacy)); - @include color.background(contrast accent, (legacy: $legacy)); - } - - tbody { - @include color.background(alt accent, (legacy: $legacy)); + &__content { + table { + thead { + th { + @include color.background(alt grey, (legacy: $legacy)); + @include color.background-image((border plain grey) (border contrast grey), (legacy: $legacy)); - tr:nth-child(even) { - @include color.background(contrast accent, (legacy: $legacy)); + &:last-child { + @include color.background(alt grey, (legacy: $legacy)); + @include color.background-image((border plain grey), (legacy: $legacy)); + } + } } - } - /* Style bordered, ajoute des bordures entre chaque ligne */ - &#{ns(table--bordered)} { tbody { tr { - @include color.background-image(border default accent, (legacy: $legacy)); + &[aria-selected=true] { + &::after { + @include color.background-image((border action-high blue-france) (border action-high blue-france) (border action-high blue-france) (border action-high blue-france), (legacy: $legacy)); + } + } + } + + th, + td { + @include color.background-image((border contrast grey) (border contrast grey), (legacy: $legacy)); + @include color.background(default grey, (legacy: $legacy)); + } + + th { + @include color.background(alt grey, (legacy: $legacy)); + @include color.background-image((border contrast grey) (border contrast grey), (legacy: $legacy)); } } } } - /* Style bordered, ajoute des bordures entre chaque ligne */ - &--bordered { - tbody { - tr { - @include color.background-image(border default grey, (legacy: $legacy)); + &__shadow--left { + #{ns(table__content)} { + #{ns(cell--fixed)} { + @include color.background-image((border contrast grey) (border plain grey), (legacy: $legacy)); - /* Style bordered, enleve le style even/odd */ - &:nth-child(even) { - @include color.transparent-background((legacy:$legacy, hover: true)); + &[scope='col'] { + @include color.background-image((border plain grey) (border plain grey), (legacy: $legacy)); + } + } + + thead { + #{ns(cell--fixed)} { + @include color.background-image((border plain grey) (border plain grey), (legacy: $legacy)); } } } } + + &__detail { + @include color.text(mention grey, (legacy:$legacy)); + } } } diff --git a/src/component/table/style/module/_default.scss b/src/component/table/style/module/_default.scss index 14bb23f97..e64eedf3b 100644 --- a/src/component/table/style/module/_default.scss +++ b/src/component/table/style/module/_default.scss @@ -3,35 +3,232 @@ /// @group Table //// +@use 'module/media-query'; +@use 'module/color'; +@use 'module/spacing'; + #{ns(table)} { - --table-offset: #{space(4v)}; + --table-offset: spacing.space(4v); + --row-height: 0; + @include set-text-margin(0); @include set-title-margin(0); - @include relative; @include margin-bottom(10v); - @include padding-top(var(--table-offset)); - @include before('', block) { - @include size(100%, 0); + &__wrapper { + @include padding(1px 1px 0); + @include margin(-1px 0); } - &:not(#{ns(table--no-scroll)}) { + &__container { + overflow: auto; + @include padding-top(var(--table-offset)); + } + + &__content { + width: 100%; + min-width: 40rem; + + #{ns(cell)} { + &--fixed { + position: sticky; + left: 0; + } + + &--center { + text-align: center; + } + + &--right { + text-align: right; + } + + &--top { + vertical-align: top; + } + + &--bottom { + vertical-align: bottom; + } + + &__title { + font-weight: 700; + } + + &__desc { + font-weight: 400; + } + + &--inline { + display: flex; + align-items: center; + @include margin(0 -2v); + + > * { + @include margin(0 2v); + } + } + + &--multiline { + &, + & * { + white-space: normal; + } + } + + &--sort { + display: flex; + align-items: center; + justify-content: space-between; + + #{ns(btn--sort)} { + @include margin-left(4v); + } + } + } + + #{ns(col)} { + &--xs { + white-space: normal; + @include min-width(16v); + } + + &--sm { + white-space: normal; + @include min-width(20v); + } + + &--md { + white-space: normal; + @include min-width(50v); + } + + &--lg { + white-space: normal; + @include min-width(100v); + } + } + table { - width: 100%; + @include size(100%); + border-spacing: 0; + + thead { + position: relative; + + @include before('', block) { + @include absolute(inherit, 0, 0 , 0, 100%, 1px); + pointer-events: none; + @include z-index(over); + } + + th { + @include font-weight(bold); + } + } + + tbody { + tr:first-child[aria-selected=true]:has(+ tr:not([aria-selected=true])), + tr:not([aria-selected=true]) + tr:last-child[aria-selected=true], + tr:not([aria-selected=true]) + tr[aria-selected=true]:has(+ tr:not([aria-selected=true])) { + &::after { + // selected borders left-right + top-bottom + background-size: 100% 2px, 2px 100%, 2px 100%, 100% 2px; + background-repeat: no-repeat, no-repeat, no-repeat, no-repeat; + background-position: 100% 100%, 0 0, 100% 0, 100% 0; + } + } + + tr[aria-selected=true] + tr[aria-selected=true] { + &::after { + // selected borders left-right + background-size: 2px 100%, 2px 100%; + background-repeat: no-repeat, no-repeat; + background-position: 0 0, 100% 0; + } + } + + tr:first-child[aria-selected=true]:has(+ tr[aria-selected=true]), + tr:not([aria-selected=true]) + tr[aria-selected=true] { + &::after { + // selected borders left-right + top + background-size: 2px 100%, 2px 100%, 100% 2px; + background-repeat: no-repeat, no-repeat, no-repeat; + background-position: 0 0, 100% 0, 100% 0; + } + } + + tr[aria-selected=true] + tr:last-child[aria-selected=true], + tr[aria-selected=true]:has(+ tr:not([aria-selected=true])) { + &::after { + // selected borders left-right + bottom + background-size: 100% 2px, 2px 100%, 2px 100%; + background-repeat: no-repeat, no-repeat, no-repeat; + background-position: 100% 100%, 0 0, 100% 0; + } + } + + tr { + &[aria-selected="true"] { + &::after { + @include absolute(null, null, null, 0, 100%, var(--row-height)); + @include z-index(above); + content: ''; + pointer-events: none; + transform: translateY(spacing.space(-0.5v)); + + // selected borders left-right + background-size: spacing.space(0.5v 100%), spacing.space(0.5v 100%); + background-repeat: no-repeat, no-repeat; + background-position: 0 0, 100% 0; + } + } + } + } } - } - table { - @include size(100%); - display: block; - overflow: auto; - border-spacing: 0; - } + th, + td { + display: table-cell; + @include padding(2v 4v); + @include text-style(sm); + text-align: left; + vertical-align: middle; + white-space: nowrap; + background-size: 100% 1px; + background-repeat: no-repeat; + background-position: 100% 100%; - &#{ns-attr(js-table, true)} { - caption { - @include absolute(0); + #{ns(checkbox-group)} { + display: inline-block; + + #{ns(label)} { + @include text-style(sm); + } + } + } + + #{ns(cell--fixed)} { + #{ns(checkbox-group)} { + & + * { + @include margin-left(2v); + } + + #{ns(label)} { + color: transparent; + @include size(0); + pointer-events: none; + + &::before { + left: spacing.space(-5v); + } + } + + input[type="checkbox"] { + left: spacing.space(1v); + } + } } } @@ -40,33 +237,30 @@ @include title-style(h4, true); @include font-weight(bold); text-align: left; + + #{ns(table__caption__desc)} { + @include margin-top(4v); + @include text-style(md); + @include font-weight(regular); + } } - td, - th { - text-align: left; - vertical-align: middle; - display: table-cell; - border: 0; - @include padding(3v); - @include padding(4v, md); + &__detail { @include text-style(sm); } - th { - font-weight: font-weight(bold); - } + &#{ns-attr(js-table, true)} { + caption { + @include absolute(0); + } - thead { - background-size: 100% 2px; - background-position: bottom; - background-repeat: no-repeat; + #{ns(table__wrapper)} { + position: relative; - td, - th { - font-weight: font-weight(bold); - @include padding-bottom(3.5v); // 0.5v for the box shadow - @include padding-bottom(4.5v, md); + @include before('', block) { + @include absolute(inherit, 0, 0 , 0, 100%, calc(100% - var(--table-offset))); + pointer-events: none; + } } } } diff --git a/src/component/table/style/module/_footer.scss b/src/component/table/style/module/_footer.scss new file mode 100644 index 000000000..ef27a15d7 --- /dev/null +++ b/src/component/table/style/module/_footer.scss @@ -0,0 +1,76 @@ +//// +/// Table Module - footer +/// @group Table +//// + +@use 'module/media-query'; + +#{ns(table)} { + &__footer { + @include media-query.respond-from(md) { + @include display-flex(row, center, space-between, wrap); + + #{ns(select-group)} { + #{ns(select)} { + @include size(64v); + } + } + + #{ns(btns-group)} { + @include padding-left(2v); + } + } + + &--start { + @include margin-top(4v); + + @include media-query.respond-from(md) { + @include display-flex(row, center, flex-start); + + #{ns(select-group)} { + @include padding-left(2v); + @include padding-right(2v); + + #{ns(select)} { + @include margin-top(0); + } + } + } + + #{ns(table__detail)} { + @include padding-right(2v); + } + } + + &--middle { + @include margin-top(4v); + + @include display-flex(row, center, center); + + @include media-query.respond-from(lg) { + display: block; + @include padding-left(2v); + @include padding-right(2v); + } + + #{ns(pagination)} { + @include margin-top(3v); + + &__list { + @include media-query.respond-from(lg) { + flex-wrap: nowrap; + } + } + } + } + + &--end { + @include media-query.respond-from(lg) { + flex: 1 0 0; + } + + @include margin-top(6v); + flex: 1 0 100%; + } + } +} diff --git a/src/component/table/style/module/_header.scss b/src/component/table/style/module/_header.scss new file mode 100644 index 000000000..a3594d013 --- /dev/null +++ b/src/component/table/style/module/_header.scss @@ -0,0 +1,46 @@ +//// +/// Table Module - footer +/// @group Table +//// + +@use 'module/media-query'; + +#{ns(table)} { + &__header { + @include media-query.respond-from(md) { + @include display-flex(row, center, space-between); + + #{ns(table__detail)} { + order: 1; + flex: 1; + @include padding-right(2v); + } + + #{ns(btns-group)} { + order: 2; + @include padding-left(2v); + @include padding-right(2v); + } + + #{ns(segmented)} { + order: 3; + flex: 0; + @include padding-left(2v); + } + } + + #{ns(btns-group)} { + @include media-query.respond-from(md) { + @include padding-right(2v); + } + } + + #{ns(table__detail)} { + @include margin-bottom(4v); + } + + #{ns(segmented)} { + @include margin-bottom(4v); + } + } +} diff --git a/src/component/table/style/module/_lg.scss b/src/component/table/style/module/_lg.scss new file mode 100644 index 000000000..016a7f0bf --- /dev/null +++ b/src/component/table/style/module/_lg.scss @@ -0,0 +1,13 @@ +//// +/// Table Module - lg +/// @group Table +//// + +#{ns(table)} { + &--lg &__content { + th, + td { + @include padding(3v 6v); + } + } +} diff --git a/src/component/table/style/module/_shadow.scss b/src/component/table/style/module/_shadow.scss index e6f890c2e..cabc7f066 100644 --- a/src/component/table/style/module/_shadow.scss +++ b/src/component/table/style/module/_shadow.scss @@ -6,68 +6,33 @@ @use 'module/selector'; /* -* Ombres ajoutées en Js si le contenu est plus grand que le conteneur +* Bordure ajoutée à gauche en Js si le contenu est plus grand que le conteneur */ -#{ns(table__shadow)} { - @include before('', block) { - @include absolute(var(--table-offset), 0, 0, 0); - @include z-index(over); - @include _table-scroll-shadow(false, false); - opacity: 0.32; - pointer-events: none; - transition: box-shadow 0.3s; - } - - /** - * Modifier ombre à gauche - **/ - &--left { - @include before { - @include _table-scroll-shadow(true, false); // @TODO: à implementer dans la mixin shadow - } - } - - /** - * Modifier ombre à droite - **/ - &--right { - @include before { - @include _table-scroll-shadow(false, true);// @TODO: à implementer dans la mixin shadow - } - } - - /** - * Modifier combinaison ombre à gauche et ombre à droite - **/ - &--left#{&}--right { - @include before('', block) { - @include _table-scroll-shadow(true, true); // @TODO: à implementer dans la mixin shadow +#{ns(table)} { + #{ns(table__shadow)} { + @include after('', block) { + @include absolute(var(--table-offset), 0, 0, 0); + @include z-index(over); + @include _table-scroll-shadow(false, false); + opacity: 0.32; + pointer-events: none; + transition: box-shadow 0.3s; } - } -} -/* -* Positionnement ombres sur le tableau sans caption -*/ -#{ns(table--no-caption)} #{ns(table__shadow)} { - @include before('', block) { - @include absolute(0, 0, 0, 0); - } -} - -/* -* Positionnement ombres sur le tableau avec caption en bas -*/ -#{ns(table--caption-bottom)} #{ns(table__shadow)} { - @include before('', block) { - @include absolute(0, 0, 0, 0); - } -} - -@include selector.theme(dark) { - #{ns(table__shadow)} { - @include before { - opacity: 1; + /** + * Modifier Bordure à gauche + **/ + &--left { + th[scope='row'] { + @include z-index(over); + } + + #{ns(cell--fixed)} { + @include z-index(over); + background-size: 100% 1px, 1px 100%; + background-repeat: no-repeat, no-repeat; + background-position: 0 100%, 100% 0; + } } } } diff --git a/src/component/table/style/module/_sm.scss b/src/component/table/style/module/_sm.scss new file mode 100644 index 000000000..5f8df38f4 --- /dev/null +++ b/src/component/table/style/module/_sm.scss @@ -0,0 +1,13 @@ +//// +/// Table Module - sm +/// @group Table +//// + +#{ns(table)} { + &--sm &__content { + th, + td { + @include padding(1v 3v); + } + } +} diff --git a/src/component/table/style/module/_variants.scss b/src/component/table/style/module/_variants.scss index e5162d460..bb93a5006 100644 --- a/src/component/table/style/module/_variants.scss +++ b/src/component/table/style/module/_variants.scss @@ -3,76 +3,74 @@ /// @group Table //// -/* -* Cache la caption -*/ -#{ns(table--no-caption)} { - @include padding-top(0); +#{ns(table)} { + /* + * Cache la caption + */ + &#{ns(table--no-caption)} { + #{ns(table__wrapper)} { + @include before { + height: 100%; + } - caption { - @include sr-only(); - @include height(0); - } -} - -/* -* Fixe le caption en bas du tableau -*/ -#{ns(table--caption-bottom)} { - @include padding-top(0); - @include margin-bottom(0); - @include margin-top(4v); - - table { - @include margin-bottom(calc(var(--table-offset) + 11v)); - } + #{ns(table__container)} { + @include padding-top(0); + } - &#{ns-attr(js-table, true)} { - caption { - @include absolute(100%, 0, 0, 0); - @include margin-top(4v); + caption { + @include sr-only(); + @include height(0); + } } } - caption { - @include margin-top(4v); - @include height(min-content); - caption-side: bottom; - } -} + /* + * Fixe le caption en bas du tableau + */ + &#{ns(table--caption-bottom)} { + @include margin-bottom(0); -/* -* pas de scroll ni de shadow -*/ -#{ns(table--no-scroll)} { - @include min-width(auto); + #{ns(table__wrapper)} { + @include margin-bottom(calc(var(--table-offset) + 6v)); - table { - overflow-x: hidden; - } + &::before { + height: 100%; + } - caption { - @include max-width(calc(100vw - 8v)); // eol in mobile even if table overflow - } -} + #{ns(table__container)} { + @include padding-top(0); + } + + caption { + @include margin-top(4v); + @include height(min-content); + caption-side: bottom; + } + } -/* -* Fixe la taille des colonnes du tableau -*/ -#{ns(table--layout-fixed)} { - table { - display: table; - table-layout: fixed; + &#{ns-attr(js-table, true)} { + caption { + @include absolute(100%, 0, 0, 0); + @include margin-top(4v); + } + } } -} -/* Style bordered, ajoute des bordures entre chaque ligne */ -#{ns(table--bordered)} { - tbody { - tr { - background-size: 100% 1px; - background-position: bottom; - background-repeat: no-repeat; + /* Style bordered, ajoute des bordures verticales entre chaque cellule */ + &#{ns(table--bordered)} { + #{ns(table__content)} { + th, + td { + background-size: 100% 1px, 1px 100%; + background-repeat: no-repeat, no-repeat; + background-position: 0 100%, 100% 0; + + &:last-child { + background-size: 100% 1px; + background-repeat: no-repeat; + background-position: 100% 100%; + } + } } } } diff --git a/src/component/table/template/ejs/table.ejs b/src/component/table/template/ejs/table.ejs index 3ebafc4b3..ee31e0513 100644 --- a/src/component/table/template/ejs/table.ejs +++ b/src/component/table/template/ejs/table.ejs @@ -1,72 +1,44 @@ <%# # paramètres Table -* table.data (array(array), required): tableau de données à deux dimension [row][col] (la première ligne (row = 0) est reservée aux ) +* table.thead (array, required): tableau de données de l'en-tête du tableau -* table.caption (string, required) : titre du tableau - -* table.noScroll (boolean, optional) : {default: false} désactive le scroll - -* table.noCaption (boolean, optional) : {default: false} cache le texte de la caption - -* table.captionBottom (boolean, optional) : {default: false} positionne la caption en bas - -* table.bordered (boolean, optional) : {default: false} si true, ajoute des séparateurs entre chaque ligne et enleve l'effet even/odd - -* table.layout (string, optional) : si non undefined, fix la taille des colonnes à 100%/col +* table.tbodies (array, required): tableau de données du corps du tableau -* table.col (integer, optional) : nombre de colones à afficher (si différent du nombre de colones de data) - -* table.row (integer, optional) : nombre de lignes à afficher (si différent du nombre de lignes de data) +* table.caption (string, required) : titre du tableau -* table.accent (string, optional): couleur d'accenturation du composant +* table.captionDetail (string, optional) : description précise du tableau -* table.id (string) : id de l'élément +* table.id (string) : id du tableau -* table.classes (array, optional): classes supplémentaires du composant +* table.classes (array, optional): classes supplémentaires du tableau -* table.attributes (array, optional): attributs supplémentaires du composant +* table.attributes (object, optional): attributs supplémentaires du tableau %> -<% eval(include('../../../../core/index.ejs')); %> -<% +<% eval(include('../../../../core/index.ejs')); + let table = locals.table || {}; let classes = table.classes || []; const attributes = table.attributes || {}; -attributes.id = table.id || uniqueId('table'); -const data = table.data || [[]]; - -classes.push(prefix + '-table'); -if (table.accent !== undefined) classes.push(prefix + '-table--' + table.accent); -if (table.bordered) classes.push(prefix + '-table--bordered'); -if (table.noScroll) classes.push(prefix + '-table--no-scroll'); -if (table.noCaption) classes.push(prefix + '-table--no-caption'); -if (table.captionBottom) classes.push(prefix + '-table--caption-bottom'); -if (table.layout) classes.push(prefix + '-table--layout-fixed'); -if (!table.row) table.row = data.length; -if (!table.col) table.col = data[0].length; +attributes.id = table.id || uniqueId('-table'); %> -
<%- includeAttrs(attributes) %>> - - <% if (table.caption !== undefined) { %> - - <% } %> - - - <% for(let col = 0; col < table.col; col++) { %> - - <% } %> - - - - <% for(let row = 1; row < table.row; row++) { %> - - <% for(let col = 0; col < table.col; col++) { %> - - <% } %> - +
<%= table.caption %>
<%- table.data[0][col] %>
<%- table.data[row][col] %>
<%- includeAttrs(attributes) %>> + <% if (table.caption !== undefined) { %> +
+ <%= table.caption %> + <% if (table.captionDetail !== undefined) { %> +
<%- table.captionDetail %>
<% } %> - -
-
+ + <% } %> + <% if (table.thead) { %> + <%- include('./thead.ejs', {thead: table.thead}); %> + <% } %> + <% if (table.tbodies) { %> + <% for (let index = 0; index < table.tbodies.length; index++) { %> + <%- include('./tbody.ejs', {tbody: {table, index}}); %> + <% } %> + <% } %> + diff --git a/src/component/table/template/ejs/tbody.ejs b/src/component/table/template/ejs/tbody.ejs new file mode 100644 index 000000000..6c3a6e4cc --- /dev/null +++ b/src/component/table/template/ejs/tbody.ejs @@ -0,0 +1,42 @@ +<%# +# paramètres Tbody + +* tbody.table (object, required): contenu du tableau + ** tbody.table.tbodies (array, required): tableau de données du tbody à trois dimensions [body][row][col] + *** tbody.table.tbodies[body][row][col].content (string, required): contenu de la cellule + *** tbody.table.tbodies[body][row][col].markup (string, optional): balise de la cellule (th, td) + *** tbody.table.tbodies[body][row][col].classes (array, optional): classes supplémentaires de la cellule + *** tbody.table.tbodies[body][row][col].attributes (array, optional): attributs supplémentaires de la cellule +%> + +<% eval(include('../../../../core/index.ejs')); + +const table = tbody.table || {}; +const index = tbody.index || 0; +%> + + + <% if (table.tbodies && table.tbodies[index]) { %> + <% for (let row = 0; row < table.tbodies[index].length; row++) { %> + <% + const rowAttributes = {}; + rowAttributes.id = `${table.id}-row-key-${row + 1}`; + rowAttributes['data-row-key'] = row + 1; + %> + > + <% if (table.tbodies[index].length) { %> + <% for (let col = 0; col < table.tbodies[index][row].length; col++) { %> + <% + const colMarkup = table.tbodies[index][row][col].markup || 'td'; + const colClasses = table.tbodies[index][row][col].classes || []; + const colAttributes = table.tbodies[index][row][col].attributes || {}; + %> + <<%- colMarkup %> <%- includeClasses(colClasses) %> <%- includeAttrs(colAttributes) %>> + <%- table.tbodies[index][row][col].content %> + > + <% } %> + <% } %> + + <% } %> + <% } %> + diff --git a/src/component/table/template/ejs/thead.ejs b/src/component/table/template/ejs/thead.ejs new file mode 100644 index 000000000..6d29f6940 --- /dev/null +++ b/src/component/table/template/ejs/thead.ejs @@ -0,0 +1,31 @@ +<%# +# paramètres Thead + +* thead (array, required): tableau de données de l'en-tête à deux dimensions [row][col] + ** thead[row][col].content (string, required): contenu de la cellule + ** thead[row][col].classes (array, optional): classes supplémentaires de la cellule + ** thead[row][col].attributes (array, optional): attributs supplémentaires de la cellule +%> + +<% eval(include('../../../../core/index.ejs')); + +let thead = locals.thead || {}; +%> + + + <% for (let row = 0; row < thead.length; row++) { %> + + <% if (thead[row] && thead[row].length) { %> + <% for (let col = 0; col < thead[row].length; col++) { %> + <% + let colClasses = thead[row][col].classes || []; + const colAttributes = thead[row][col].attributes || {}; + %> + <%- includeAttrs(colAttributes) %>> + <%- thead[row][col].content %> + + <% } %> + <% } %> + + <% } %> + diff --git a/src/component/table/template/ejs/wrapper.ejs b/src/component/table/template/ejs/wrapper.ejs new file mode 100644 index 000000000..10f9f5703 --- /dev/null +++ b/src/component/table/template/ejs/wrapper.ejs @@ -0,0 +1,54 @@ +<%# +# paramètres Table wrapper + +* wrapper.table (object, required) : contenu du tableau + +* wrapper.noCaption (boolean, optional) : {default: false} cache le texte de la caption + +* wrapper.captionBottom (boolean, optional) : {default: false} positionne la caption en bas + +* wrapper.bordered (boolean, optional) : {default: false} si true, ajoute des séparateurs entre chaque ligne et enleve l'effet even/odd + +* wrapper.classes (array, optional): classes supplémentaires du composant + +* wrapper.attributes (object, optional): attributs supplémentaires du composant + +* wrapper.header (boolean, optional): {default: false} affiche l'en-tête du composant + +* wrapper.footer (boolean, optional): {default: false} affiche le pied de page du composant +%> + +<% eval(include('../../../../core/index.ejs')); + +let wrapper = locals.table || {}; +let classes = wrapper.classes || []; + +const attributes = wrapper.attributes || {}; + +const table = wrapper.table || [[]]; +attributes.id = table.id + '-component'; + +classes.push(prefix + '-table'); + +if (wrapper.noCaption) classes.push(prefix + '-table--no-caption'); +if (wrapper.captionBottom) classes.push(prefix + '-table--caption-bottom'); +if (wrapper.bordered) classes.push(prefix + '-table--bordered'); +%> + +
<%- includeAttrs(attributes) %>> + <% if (wrapper.header !== undefined) { %> +
<%- wrapper.header %>
+ <% } %> + <% if (table) { %> +
+
+
+ <%- include('./table.ejs', {table: table}); %> +
+
+
+ <% } %> + <% if (wrapper.footer !== undefined) { %> + + <% } %> +
diff --git a/src/component/tooltip/style/_module.scss b/src/component/tooltip/style/_module.scss index f619ba16f..69c0189ad 100644 --- a/src/component/tooltip/style/_module.scss +++ b/src/component/tooltip/style/_module.scss @@ -20,6 +20,7 @@ background-repeat: no-repeat; background-position: spacing.space(calc(50% + var(--arrow-x)) calc(100% - 2v)), spacing.space(calc(50% + var(--arrow-x)) calc(100% - $decimalVUnity)), spacing.space(50% calc(100% - 3v)), spacing.space(50% calc(100% - 3v)); background-size: spacing.space(2v 1.5v), spacing.space(2v 1.5v), 100% 1px, spacing.space(100% calc(100% - 3v)); + white-space: normal; &:not(&--shown) { // transition in/out diff --git a/src/core/icon/system/arrow-up-down-line.svg b/src/core/icon/system/arrow-up-down-line.svg new file mode 100644 index 000000000..fd32062ae --- /dev/null +++ b/src/core/icon/system/arrow-up-down-line.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/core/script/collapse/collapse.js b/src/core/script/collapse/collapse.js index 372a4e807..20e59f016 100644 --- a/src/core/script/collapse/collapse.js +++ b/src/core/script/collapse/collapse.js @@ -19,42 +19,43 @@ class Collapse extends Disclosure { init () { super.init(); - this.listen('transitionend', this.transitionend.bind(this)); + this.listen('transitionend', this.endCollapsing.bind(this)); } - transitionend (e) { + endCollapsing (e) { + if (!this._isCollpasing) return; + if (this._timeout) clearTimeout(this._timeout); + this._timeout = null; + this._isCollpasing = false; this.removeClass(CollapseSelector.COLLAPSING); if (!this.isDisclosed) { if (this.isLegacy) this.style.maxHeight = ''; - else this.style.removeProperty('--collapse-max-height'); } } unbound () { if (this.isLegacy) this.style.maxHeight = 'none'; - else this.style.setProperty('--collapse-max-height', 'none'); } disclose (withhold) { if (this.isDisclosed === true || !this.isEnabled) return false; this.unbound(); - this.request(() => { - this.addClass(CollapseSelector.COLLAPSING); - this.adjust(); - this.request(() => { - super.disclose(withhold); - }); - }); + this.collapsing(() => super.disclose(withhold)); } conceal (withhold, preventFocus) { if (this.isDisclosed === false) return false; + this.collapsing(() => super.conceal(withhold, preventFocus)); + } + + collapsing (request) { + this._isCollpasing = true; + if (this._timeout) clearTimeout(this._timeout); + this.addClass(CollapseSelector.COLLAPSING); + this.adjust(); this.request(() => { - this.addClass(CollapseSelector.COLLAPSING); - this.adjust(); - this.request(() => { - super.conceal(withhold, preventFocus); - }); + request(); + this._timeout = setTimeout(this.endCollapsing.bind(this), 500); }); } diff --git a/src/core/style/collapse/_tool.scss b/src/core/style/collapse/_tool.scss index 417449ccc..55c2c5779 100644 --- a/src/core/style/collapse/_tool.scss +++ b/src/core/style/collapse/_tool.scss @@ -4,13 +4,13 @@ //// @mixin collapse($className: collapse) { - --collapse-max-height: 0; --collapse: -99999px; --collapser: ''; + --collapse-max-height: none; overflow: hidden; transition: visibility 0.3s; - max-height: 0; + // max-height: 0; max-height: var(--collapse-max-height); &::before { @@ -21,6 +21,10 @@ margin-top: 0; } + #{&}:not(&--expanded):not(#{ns(collapsing)}) { + --collapse-max-height: 0; + } + #{&}:not(&--expanded) { visibility: hidden;