@1024pix/pix-ui 18.2.0 → 19.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/addon/components/pix-checkbox.hbs +12 -7
- package/addon/components/pix-checkbox.js +0 -9
- package/addon/components/pix-multi-select.hbs +45 -27
- package/addon/components/pix-multi-select.js +29 -26
- package/addon/styles/_a11y.scss +10 -0
- package/addon/styles/_form.scss +1 -1
- package/addon/styles/_pix-checkbox.scss +4 -5
- package/addon/styles/_pix-modal.scss +26 -14
- package/addon/styles/_pix-multi-select.scss +6 -57
- package/addon/styles/_pix-sidebar.scss +0 -4
- package/addon/styles/_pix-stars.scss +1 -11
- package/addon/styles/addon.scss +1 -0
- package/app/modifiers/on-arrow-down-up-action.js +82 -0
- package/app/modifiers/on-enter-action.js +28 -0
- package/app/modifiers/on-escape-action.js +7 -3
- package/app/modifiers/trap-focus.js +2 -2
- package/app/stories/form.stories.js +21 -1
- package/app/stories/pix-checkbox.stories.js +3 -4
- package/app/stories/pix-checkbox.stories.mdx +3 -2
- package/app/stories/pix-modal.stories.js +1 -1
- package/app/stories/pix-modal.stories.mdx +1 -0
- package/app/stories/pix-multi-select.stories.js +50 -61
- package/app/stories/pix-multi-select.stories.mdx +5 -2
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Pix-UI Changelog
|
|
2
2
|
|
|
3
|
+
## v19.0.1 (04/10/2022)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### :rocket: Amélioration
|
|
7
|
+
- [#258](https://github.com/1024pix/pix-ui/pull/258) [FEATURE] Centre la PixModal en hauteur
|
|
8
|
+
- [#257](https://github.com/1024pix/pix-ui/pull/257) [FEATURE] Suppression du `padding` de la Sidebar pour permettre de mieux customiser
|
|
9
|
+
|
|
10
|
+
### :coffee: Autre
|
|
11
|
+
- [#256](https://github.com/1024pix/pix-ui/pull/256) [BUFGIX] Mettre à jour la page d'utilisation de la PixModal (PIX-5735)
|
|
12
|
+
|
|
13
|
+
## v19.0.0 (16/09/2022)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### :rocket: Amélioration
|
|
17
|
+
- [#252](https://github.com/1024pix/pix-ui/pull/252) [FEATURE] Rendre accessible (a11y) le composant PixMultiSelect (PIX-5555)
|
|
18
|
+
|
|
3
19
|
## v18.2.0 (16/09/2022)
|
|
4
20
|
|
|
5
21
|
|
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
<div class="pix-checkbox">
|
|
2
|
-
<label
|
|
3
|
-
class="pix-checkbox__label {{this.labelSizeClass}} {{if @screenReaderOnly 'sr-only'}}"
|
|
4
|
-
for={{this.id}}
|
|
5
|
-
>
|
|
6
|
-
{{this.label}}
|
|
7
|
-
</label>
|
|
8
2
|
<input
|
|
9
|
-
id={{
|
|
3
|
+
id={{@id}}
|
|
10
4
|
type="checkbox"
|
|
11
5
|
class="pix-checkbox__input {{if @isIndeterminate ' pix-checkbox__input--indeterminate'}}"
|
|
12
6
|
checked={{@checked}}
|
|
13
7
|
...attributes
|
|
14
8
|
/>
|
|
9
|
+
<label
|
|
10
|
+
class="pix-checkbox__label {{this.labelSizeClass}} {{if @screenReaderOnly 'sr-only'}}"
|
|
11
|
+
for={{@id}}
|
|
12
|
+
>
|
|
13
|
+
{{#if (has-block)}}
|
|
14
|
+
{{yield}}
|
|
15
|
+
{{else}}
|
|
16
|
+
yield required to give a label for PixCheckBox
|
|
17
|
+
{{@id}}.
|
|
18
|
+
{{/if}}
|
|
19
|
+
</label>
|
|
15
20
|
</div>
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import Component from '@glimmer/component';
|
|
2
|
-
import { htmlSafe } from '@ember/string';
|
|
3
2
|
|
|
4
3
|
const labelSizeToClass = new Map([
|
|
5
4
|
['small', 'pix-checkbox__label--small'],
|
|
@@ -11,17 +10,9 @@ export default class PixCheckbox extends Component {
|
|
|
11
10
|
constructor() {
|
|
12
11
|
super(...arguments);
|
|
13
12
|
|
|
14
|
-
if (!this.args.label) {
|
|
15
|
-
throw new Error('ERROR in PixCheckbox component, you must provide @label params');
|
|
16
|
-
}
|
|
17
|
-
this.label = htmlSafe(this.args.label);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
get id() {
|
|
21
13
|
if (!this.args.id || !this.args.id.toString().trim()) {
|
|
22
14
|
throw new Error('ERROR in PixCheckbox component, @id param is not provided');
|
|
23
15
|
}
|
|
24
|
-
return this.args.id;
|
|
25
16
|
}
|
|
26
17
|
|
|
27
18
|
get labelSizeClass() {
|
|
@@ -1,35 +1,51 @@
|
|
|
1
|
-
<div
|
|
2
|
-
|
|
3
|
-
{{
|
|
4
|
-
|
|
5
|
-
{{
|
|
1
|
+
<div
|
|
2
|
+
class="pix-multi-select"
|
|
3
|
+
{{on-click-outside this.hideDropDown}}
|
|
4
|
+
{{on-arrow-down-up-action this.listId this.showDropDown this.isExpanded}}
|
|
5
|
+
{{on-escape-action this.hideDropDown @id}}
|
|
6
|
+
>
|
|
6
7
|
|
|
7
8
|
{{#if @isSearchable}}
|
|
8
|
-
<label
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
<label
|
|
10
|
+
for={{@id}}
|
|
11
|
+
class="pix-multi-select__label{{if @screenReaderOnly ' screen-reader-only'}}"
|
|
12
|
+
>{{@label}}</label>
|
|
13
|
+
<span class="pix-multi-select-header">
|
|
11
14
|
<FaIcon @icon="magnifying-glass" class="pix-multi-select-header__search-icon" />
|
|
12
15
|
|
|
13
16
|
<input
|
|
17
|
+
class="pix-multi-select-header__search-input"
|
|
14
18
|
id={{@id}}
|
|
15
19
|
type="text"
|
|
16
20
|
name={{@id}}
|
|
17
|
-
placeholder={{this.
|
|
21
|
+
placeholder={{this.innerText}}
|
|
18
22
|
autocomplete="off"
|
|
19
23
|
{{on "input" this.updateSearch}}
|
|
20
|
-
{{on "
|
|
21
|
-
|
|
24
|
+
{{on "click" this.toggleDropDown}}
|
|
25
|
+
aria-expanded={{this.isAriaExpanded}}
|
|
26
|
+
aria-controls={{this.listId}}
|
|
27
|
+
aria-haspopup="menu"
|
|
28
|
+
...attributes
|
|
22
29
|
/>
|
|
23
|
-
|
|
24
|
-
</label>
|
|
30
|
+
</span>
|
|
25
31
|
{{else}}
|
|
32
|
+
<span
|
|
33
|
+
id={{this.labelId}}
|
|
34
|
+
class="pix-multi-select__label{{if @screenReaderOnly ' screen-reader-only'}}"
|
|
35
|
+
>{{@label}}</span>
|
|
36
|
+
|
|
26
37
|
<button
|
|
38
|
+
aria-labelledby={{this.labelId}}
|
|
27
39
|
id={{@id}}
|
|
28
40
|
type="button"
|
|
29
|
-
|
|
41
|
+
aria-expanded={{this.isAriaExpanded}}
|
|
42
|
+
aria-controls={{this.listId}}
|
|
43
|
+
aria-haspopup="menu"
|
|
44
|
+
class="pix-multi-select-header"
|
|
30
45
|
{{on "click" this.toggleDropDown}}
|
|
46
|
+
...attributes
|
|
31
47
|
>
|
|
32
|
-
{{
|
|
48
|
+
{{this.innerText}}
|
|
33
49
|
<FaIcon
|
|
34
50
|
class="pix-multi-select-header__dropdown-icon
|
|
35
51
|
{{if this.isExpanded ' pix-multi-select-header__dropdown-icon--expand'}}"
|
|
@@ -38,23 +54,25 @@
|
|
|
38
54
|
</button>
|
|
39
55
|
{{/if}}
|
|
40
56
|
|
|
41
|
-
<ul
|
|
57
|
+
<ul
|
|
58
|
+
class="pix-multi-select-list {{unless this.isExpanded 'pix-multi-select-list--hidden'}}"
|
|
59
|
+
id={{this.listId}}
|
|
60
|
+
aria-multiselectable="true"
|
|
61
|
+
role="menu"
|
|
62
|
+
>
|
|
42
63
|
{{#if (gt this.results.length 0)}}
|
|
43
64
|
{{#each this.results as |option|}}
|
|
44
|
-
<li class="pix-multi-select-list__item">
|
|
45
|
-
<
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
type="checkbox"
|
|
49
|
-
checked={{option.checked}}
|
|
50
|
-
id={{concat @id "-" option.value}}
|
|
51
|
-
name={{option.label}}
|
|
65
|
+
<li class="pix-multi-select-list__item" role="menuitem">
|
|
66
|
+
<PixCheckbox
|
|
67
|
+
@id={{concat @id "-" option.value}}
|
|
68
|
+
@checked={{option.checked}}
|
|
52
69
|
value={{option.value}}
|
|
53
70
|
{{on "change" this.onSelect}}
|
|
54
|
-
|
|
55
|
-
|
|
71
|
+
{{on-enter-action this.hideDropDown @id}}
|
|
72
|
+
tabindex={{if this.isExpanded "0" "-1"}}
|
|
73
|
+
>
|
|
56
74
|
{{yield option}}
|
|
57
|
-
</
|
|
75
|
+
</PixCheckbox>
|
|
58
76
|
</li>
|
|
59
77
|
{{/each}}
|
|
60
78
|
{{else if this.isLoadingOptions}}
|
|
@@ -26,35 +26,47 @@ export default class PixMultiSelect extends Component {
|
|
|
26
26
|
|
|
27
27
|
constructor(...args) {
|
|
28
28
|
super(...args);
|
|
29
|
-
const { onLoadOptions,
|
|
29
|
+
const { onLoadOptions, id, label, innerText } = this.args;
|
|
30
|
+
|
|
31
|
+
const idIsNotDefined = !id || !id.trim();
|
|
32
|
+
const labelIsNotDefined = !label || !label.trim();
|
|
33
|
+
const innerTextIsNotDefined = !innerText || !innerText.trim();
|
|
34
|
+
|
|
35
|
+
if (idIsNotDefined || labelIsNotDefined || innerTextIsNotDefined) {
|
|
36
|
+
const missingParams = [];
|
|
37
|
+
|
|
38
|
+
if (idIsNotDefined) missingParams.push('@id');
|
|
39
|
+
if (labelIsNotDefined) missingParams.push('@label');
|
|
40
|
+
if (innerTextIsNotDefined) missingParams.push('@innerText');
|
|
41
|
+
|
|
42
|
+
throw new Error(
|
|
43
|
+
`ERROR in PixMultiSelect component, ${missingParams.join(', ')} ${
|
|
44
|
+
missingParams.length > 1 ? 'params are' : 'param is'
|
|
45
|
+
} necessary`
|
|
46
|
+
);
|
|
47
|
+
}
|
|
30
48
|
|
|
31
49
|
if (onLoadOptions) {
|
|
32
50
|
this.isLoadingOptions = true;
|
|
33
51
|
onLoadOptions().then((options = []) => {
|
|
34
52
|
this.options = options;
|
|
35
|
-
this._setDisplayedOptions(selected, true);
|
|
36
53
|
this.isLoadingOptions = false;
|
|
37
54
|
});
|
|
38
55
|
} else {
|
|
39
56
|
this.options = [...(this.args.options || [])];
|
|
40
|
-
this._setDisplayedOptions(selected, true);
|
|
41
57
|
}
|
|
42
58
|
}
|
|
43
59
|
|
|
44
|
-
get
|
|
45
|
-
|
|
46
|
-
|
|
60
|
+
get listId() {
|
|
61
|
+
return `list-${this.args.id}`;
|
|
62
|
+
}
|
|
47
63
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
'ERROR in PixMultiSelect component, @id param is necessary when giving @label'
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
return this.args.label || null;
|
|
64
|
+
get labelId() {
|
|
65
|
+
return `label-${this.args.id}`;
|
|
54
66
|
}
|
|
55
67
|
|
|
56
|
-
get
|
|
57
|
-
return this.
|
|
68
|
+
get isAriaExpanded() {
|
|
69
|
+
return this.isExpanded ? 'true' : 'false';
|
|
58
70
|
}
|
|
59
71
|
|
|
60
72
|
get results() {
|
|
@@ -64,8 +76,8 @@ export default class PixMultiSelect extends Component {
|
|
|
64
76
|
return this.options;
|
|
65
77
|
}
|
|
66
78
|
|
|
67
|
-
get
|
|
68
|
-
const { selected,
|
|
79
|
+
get innerText() {
|
|
80
|
+
const { selected, innerText } = this.args;
|
|
69
81
|
if (selected?.length > 0) {
|
|
70
82
|
const selectedOptionLabels = this.options
|
|
71
83
|
.filter(({ value, label }) => {
|
|
@@ -76,7 +88,7 @@ export default class PixMultiSelect extends Component {
|
|
|
76
88
|
.join(', ');
|
|
77
89
|
return selectedOptionLabels;
|
|
78
90
|
}
|
|
79
|
-
return
|
|
91
|
+
return innerText;
|
|
80
92
|
}
|
|
81
93
|
|
|
82
94
|
_setDisplayedOptions(selected, shouldSort) {
|
|
@@ -108,8 +120,6 @@ export default class PixMultiSelect extends Component {
|
|
|
108
120
|
selected = selected.filter((value) => value !== event.target.value);
|
|
109
121
|
}
|
|
110
122
|
|
|
111
|
-
this._setDisplayedOptions(selected, false);
|
|
112
|
-
|
|
113
123
|
if (this.args.onSelect) {
|
|
114
124
|
this.args.onSelect(selected);
|
|
115
125
|
}
|
|
@@ -142,13 +152,6 @@ export default class PixMultiSelect extends Component {
|
|
|
142
152
|
this.isExpanded = false;
|
|
143
153
|
}
|
|
144
154
|
|
|
145
|
-
@action
|
|
146
|
-
focusDropdown() {
|
|
147
|
-
if (this.args.isSearchable && this.args.showOptionsOnInput) {
|
|
148
|
-
this.showDropDown();
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
155
|
@action
|
|
153
156
|
updateSearch(event) {
|
|
154
157
|
this.searchData = this.args.strictSearch
|
package/addon/styles/_form.scss
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
.pix-checkbox {
|
|
2
2
|
@import 'reset-css';
|
|
3
|
-
display: flex;
|
|
4
3
|
align-items: center;
|
|
5
4
|
display: flex;
|
|
6
|
-
flex-direction: row-reverse;
|
|
7
5
|
|
|
8
6
|
label, input {
|
|
9
7
|
cursor: pointer;
|
|
@@ -31,9 +29,7 @@
|
|
|
31
29
|
min-height: 18px;
|
|
32
30
|
border: 1.2px solid $pix-neutral-90;
|
|
33
31
|
border-radius: 2px;
|
|
34
|
-
|
|
35
|
-
justify-content: center;
|
|
36
|
-
align-items: center;
|
|
32
|
+
position: relative;
|
|
37
33
|
|
|
38
34
|
&:hover, &:focus {
|
|
39
35
|
box-shadow: inset 0 1px 3px rgba(0,0,0, .1), 0 0 0 6px $pix-neutral-15;
|
|
@@ -54,6 +50,9 @@
|
|
|
54
50
|
background-position: center;
|
|
55
51
|
width: 16px;
|
|
56
52
|
height: 16px;
|
|
53
|
+
position: absolute;
|
|
54
|
+
top: 0;
|
|
55
|
+
left: 0;
|
|
57
56
|
|
|
58
57
|
}
|
|
59
58
|
}
|
|
@@ -1,14 +1,27 @@
|
|
|
1
1
|
.pix-modal__overlay {
|
|
2
|
-
|
|
2
|
+
position: fixed;
|
|
3
|
+
z-index: 1000;
|
|
4
|
+
top: 0;
|
|
3
5
|
bottom: 0;
|
|
4
6
|
left: 0;
|
|
5
|
-
overflow-y: scroll;
|
|
6
|
-
position: fixed;
|
|
7
7
|
right: 0;
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
overflow-y: auto;
|
|
9
|
+
text-align: center; // Used to center horizontally the inline-block modal content
|
|
10
|
+
padding: $spacing-xs 0;
|
|
11
|
+
background-color: rgba(52, 69, 99, 0.7);
|
|
10
12
|
transition: all 0.3s ease-in-out;
|
|
11
13
|
|
|
14
|
+
// This block is used to center vertically the modal
|
|
15
|
+
// if the content is less than 100vh
|
|
16
|
+
// Inspired by https://mui.com/material-ui/react-dialog/#scrolling-long-content
|
|
17
|
+
&::after {
|
|
18
|
+
content: "";
|
|
19
|
+
display: inline-block;
|
|
20
|
+
vertical-align: middle;
|
|
21
|
+
height: 100%;
|
|
22
|
+
width: 0px;
|
|
23
|
+
}
|
|
24
|
+
|
|
12
25
|
&--hidden {
|
|
13
26
|
visibility: hidden;
|
|
14
27
|
opacity: 0;
|
|
@@ -23,16 +36,19 @@ $button-margin: 16px;
|
|
|
23
36
|
|
|
24
37
|
.pix-modal {
|
|
25
38
|
@import 'reset-css';
|
|
39
|
+
display: inline-block;
|
|
40
|
+
vertical-align: middle; // Centered vertically with the .pix-modal__overlay::after which is 100% height
|
|
41
|
+
width: 512px;
|
|
42
|
+
max-width: calc(100% - #{2 * $spacing-xs}); // Horizontal margin sets here to have extra space for the .pix-modal__overlay::after on mobile
|
|
43
|
+
text-align: initial;
|
|
44
|
+
background-color: $pix-neutral-10;
|
|
26
45
|
box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.1);
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
width: calc(100% - 24px);
|
|
46
|
+
border-radius: 4px;
|
|
47
|
+
overflow: hidden;
|
|
30
48
|
|
|
31
49
|
&__header {
|
|
32
50
|
background: $pix-neutral-0;
|
|
33
51
|
border-bottom: 1px solid $pix-neutral-20;
|
|
34
|
-
border-top-left-radius: 4px;
|
|
35
|
-
border-top-right-radius: 4px;
|
|
36
52
|
padding: $modal-padding;
|
|
37
53
|
display: flex;
|
|
38
54
|
align-items: flex-start;
|
|
@@ -64,7 +80,6 @@ $button-margin: 16px;
|
|
|
64
80
|
}
|
|
65
81
|
|
|
66
82
|
&__content {
|
|
67
|
-
background-color: $pix-neutral-10;
|
|
68
83
|
padding: $modal-padding;
|
|
69
84
|
font-size: 0.875rem;
|
|
70
85
|
color: $pix-neutral-90;
|
|
@@ -78,10 +93,7 @@ $button-margin: 16px;
|
|
|
78
93
|
}
|
|
79
94
|
|
|
80
95
|
&__footer {
|
|
81
|
-
background-color: $pix-neutral-10;
|
|
82
96
|
padding: 0 $modal-padding $modal-padding - $button-margin;
|
|
83
|
-
border-bottom-left-radius: 4px;
|
|
84
|
-
border-bottom-right-radius: 4px;
|
|
85
97
|
|
|
86
98
|
@include device-is('tablet') {
|
|
87
99
|
display: flex;
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
|
|
72
72
|
.pix-multi-select-list {
|
|
73
73
|
width: 100%;
|
|
74
|
-
margin:
|
|
74
|
+
margin: 0;
|
|
75
75
|
z-index: 200;
|
|
76
76
|
background-color: $pix-neutral-0;
|
|
77
77
|
position: absolute;
|
|
@@ -82,9 +82,11 @@
|
|
|
82
82
|
list-style-type: none;
|
|
83
83
|
padding: 0;
|
|
84
84
|
box-shadow: 0px 8px 24px 0px rgba(23, 43, 77, 0.1);
|
|
85
|
+
transition: all .1s ease-in-out;
|
|
85
86
|
|
|
86
87
|
&--hidden {
|
|
87
|
-
|
|
88
|
+
visibility: hidden;
|
|
89
|
+
opacity: 0;
|
|
88
90
|
}
|
|
89
91
|
|
|
90
92
|
&::-webkit-scrollbar {
|
|
@@ -109,9 +111,10 @@
|
|
|
109
111
|
border-bottom: 1px solid $pix-neutral-15;
|
|
110
112
|
list-style: none;
|
|
111
113
|
font-size: 0.9rem;
|
|
114
|
+
padding: 8px;
|
|
112
115
|
|
|
113
116
|
@include hoverFormElement();
|
|
114
|
-
|
|
117
|
+
|
|
115
118
|
&--no-result {
|
|
116
119
|
text-align: center;
|
|
117
120
|
padding: 12px 0;
|
|
@@ -121,58 +124,4 @@
|
|
|
121
124
|
border-bottom: none;
|
|
122
125
|
}
|
|
123
126
|
}
|
|
124
|
-
|
|
125
|
-
&__checkbox {
|
|
126
|
-
position: absolute;
|
|
127
|
-
opacity: 0;
|
|
128
|
-
|
|
129
|
-
& + label {
|
|
130
|
-
position: relative;
|
|
131
|
-
display: flex;
|
|
132
|
-
align-items: center;
|
|
133
|
-
padding: 11px 16px;
|
|
134
|
-
cursor: pointer;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
& + label:before {
|
|
138
|
-
content: '';
|
|
139
|
-
margin-right: 12px;
|
|
140
|
-
display: inline-block;
|
|
141
|
-
vertical-align: text-top;
|
|
142
|
-
min-width: 16px;
|
|
143
|
-
min-height: 16px;
|
|
144
|
-
border-radius: 4px;
|
|
145
|
-
background: $pix-neutral-0;
|
|
146
|
-
border: 1px solid $pix-neutral-20;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
&:checked + label:before {
|
|
150
|
-
background: $pix-primary;
|
|
151
|
-
border-color: $pix-primary;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
&:checked + label:after {
|
|
155
|
-
position: absolute;
|
|
156
|
-
top: calc(50% - 5px);
|
|
157
|
-
left: 21px;
|
|
158
|
-
width: 7px;
|
|
159
|
-
height: 5px;
|
|
160
|
-
background: transparent;
|
|
161
|
-
border: 2px solid $pix-neutral-0;
|
|
162
|
-
border-top: none;
|
|
163
|
-
border-right: none;
|
|
164
|
-
transform: rotate(-45deg);
|
|
165
|
-
content: '';
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
&--searchable {
|
|
169
|
-
& + label {
|
|
170
|
-
padding: 11px 36px;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
&:checked + label:after {
|
|
174
|
-
left: 41px;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
127
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
.pix-stars {
|
|
2
2
|
@import 'reset-css';
|
|
3
|
+
line-height: 0;
|
|
3
4
|
|
|
4
5
|
> svg {
|
|
5
6
|
width: 36px;
|
|
@@ -30,14 +31,3 @@
|
|
|
30
31
|
fill: $pix-neutral-15;
|
|
31
32
|
}
|
|
32
33
|
}
|
|
33
|
-
|
|
34
|
-
.sr-only {
|
|
35
|
-
position: absolute;
|
|
36
|
-
width: 1px;
|
|
37
|
-
height: 1px;
|
|
38
|
-
padding: 0;
|
|
39
|
-
margin: -1px;
|
|
40
|
-
overflow: hidden;
|
|
41
|
-
clip: rect(0, 0, 0, 0);
|
|
42
|
-
border: 0;
|
|
43
|
-
}
|
package/addon/styles/addon.scss
CHANGED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { modifier } from 'ember-modifier';
|
|
2
|
+
|
|
3
|
+
export default modifier((element, [elementId, callback, isExpanded]) => {
|
|
4
|
+
const elementToTarget = document.getElementById(elementId);
|
|
5
|
+
|
|
6
|
+
element.addEventListener('keydown', handleKeyDown);
|
|
7
|
+
|
|
8
|
+
return () => {
|
|
9
|
+
element.removeEventListener('keydown', handleKeyDown);
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function handleKeyDown(event) {
|
|
13
|
+
const ARROW_UP_KEY = 'ArrowUp';
|
|
14
|
+
const ARROW_DOWN_KEY = 'ArrowDown';
|
|
15
|
+
|
|
16
|
+
if (![ARROW_UP_KEY, ARROW_DOWN_KEY].includes(event.key)) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
event.preventDefault();
|
|
20
|
+
|
|
21
|
+
const focusElement = () => {
|
|
22
|
+
const focusableElements = findFocusableElements(elementToTarget);
|
|
23
|
+
|
|
24
|
+
const [firstFocusableElement] = focusableElements;
|
|
25
|
+
const lastFocusableElement = focusableElements[focusableElements.length - 1];
|
|
26
|
+
|
|
27
|
+
const activeIndexElement = focusableElements.findIndex((elementToTarget) => {
|
|
28
|
+
return document.activeElement === elementToTarget;
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const handleArrowDown = () => {
|
|
32
|
+
if (
|
|
33
|
+
!isExpanded ||
|
|
34
|
+
document.activeElement === lastFocusableElement ||
|
|
35
|
+
activeIndexElement === -1
|
|
36
|
+
) {
|
|
37
|
+
firstFocusableElement.focus();
|
|
38
|
+
} else {
|
|
39
|
+
focusableElements[activeIndexElement + 1].focus();
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const handleArrowUp = () => {
|
|
44
|
+
if (
|
|
45
|
+
!isExpanded ||
|
|
46
|
+
document.activeElement === firstFocusableElement ||
|
|
47
|
+
activeIndexElement === -1
|
|
48
|
+
) {
|
|
49
|
+
lastFocusableElement.focus();
|
|
50
|
+
} else {
|
|
51
|
+
focusableElements[activeIndexElement - 1].focus();
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
if (ARROW_UP_KEY === event.key) {
|
|
56
|
+
handleArrowUp();
|
|
57
|
+
} else if (ARROW_DOWN_KEY === event.key) {
|
|
58
|
+
handleArrowDown();
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
if (!isExpanded) {
|
|
63
|
+
elementToTarget.addEventListener('transitionend', focusElement);
|
|
64
|
+
|
|
65
|
+
callback();
|
|
66
|
+
|
|
67
|
+
return () => {
|
|
68
|
+
elementToTarget.removeEventListener('transitionend', focusElement);
|
|
69
|
+
};
|
|
70
|
+
} else {
|
|
71
|
+
focusElement(elementToTarget);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
function findFocusableElements(element) {
|
|
77
|
+
return [
|
|
78
|
+
...element.querySelectorAll(
|
|
79
|
+
'a[href], button, input, textarea, select, details,[tabindex]:not([tabindex="-1"])'
|
|
80
|
+
),
|
|
81
|
+
].filter((el) => !el.hasAttribute('disabled') && !el.getAttribute('aria-hidden'));
|
|
82
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { modifier } from 'ember-modifier';
|
|
2
|
+
|
|
3
|
+
export default modifier((element, [callback, focusId = null]) => {
|
|
4
|
+
function handleKeyUp(event) {
|
|
5
|
+
const ENTER_KEY = 'Enter';
|
|
6
|
+
|
|
7
|
+
if (event.key !== ENTER_KEY) {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (element.type === 'checkbox') {
|
|
12
|
+
element.checked = !element.checked;
|
|
13
|
+
element.dispatchEvent(new Event('change'));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (focusId) {
|
|
17
|
+
document.getElementById(focusId).focus();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
callback(event);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
element.addEventListener('keydown', handleKeyUp);
|
|
24
|
+
|
|
25
|
+
return () => {
|
|
26
|
+
element.removeEventListener('keydown', handleKeyUp);
|
|
27
|
+
};
|
|
28
|
+
});
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import { modifier } from 'ember-modifier';
|
|
2
2
|
|
|
3
|
-
export default modifier((element, [callback]) => {
|
|
3
|
+
export default modifier((element, [callback, focusId = null]) => {
|
|
4
4
|
function handleKeyUp(event) {
|
|
5
|
-
const
|
|
5
|
+
const ESCAPE_KEY = 'Escape';
|
|
6
6
|
|
|
7
|
-
if (event.key !==
|
|
7
|
+
if (event.key !== ESCAPE_KEY) {
|
|
8
8
|
return;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
callback(event);
|
|
12
|
+
|
|
13
|
+
if (focusId) {
|
|
14
|
+
document.getElementById(focusId).focus();
|
|
15
|
+
}
|
|
12
16
|
}
|
|
13
17
|
|
|
14
18
|
element.addEventListener('keyup', handleKeyUp);
|
|
@@ -81,12 +81,12 @@ function hasDurationByKey(element, key) {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
function handleKeyDown(event, element) {
|
|
84
|
-
const TAB_KEY =
|
|
84
|
+
const TAB_KEY = 'Tab';
|
|
85
85
|
const focusableElements = findFocusableElements(element);
|
|
86
86
|
const [firstFocusableElement] = focusableElements;
|
|
87
87
|
const lastFocusableElement = focusableElements[focusableElements.length - 1];
|
|
88
88
|
|
|
89
|
-
if (event.
|
|
89
|
+
if (event.key !== TAB_KEY) {
|
|
90
90
|
return;
|
|
91
91
|
}
|
|
92
92
|
|
|
@@ -11,7 +11,7 @@ export const form = (args) => {
|
|
|
11
11
|
<br>
|
|
12
12
|
|
|
13
13
|
<PixMultiSelect
|
|
14
|
-
@
|
|
14
|
+
@innerText="Votre notation en étoiles..."
|
|
15
15
|
@id="form__pix-mutli-select"
|
|
16
16
|
@label="A quel point aimez vous Pix UI ?"
|
|
17
17
|
@onSelect={{onSelect}}
|
|
@@ -22,6 +22,19 @@ export const form = (args) => {
|
|
|
22
22
|
</PixMultiSelect>
|
|
23
23
|
<br><br>
|
|
24
24
|
|
|
25
|
+
<PixMultiSelect
|
|
26
|
+
@innerText="Mes condiements"
|
|
27
|
+
@id="form__pix-mutli-select-searchable"
|
|
28
|
+
@label="Choississez vos condiments"
|
|
29
|
+
@onSelect={{onSelect}}
|
|
30
|
+
@selected={{selected}}
|
|
31
|
+
@isSearchable={{true}}
|
|
32
|
+
@options={{optionsSearch}} as |condiment|
|
|
33
|
+
>
|
|
34
|
+
{{condiment.label}}
|
|
35
|
+
</PixMultiSelect>
|
|
36
|
+
<br><br>
|
|
37
|
+
|
|
25
38
|
<PixSelect
|
|
26
39
|
@id="form__searchable-pix-select"
|
|
27
40
|
@label="Votre fruit préféré est : "
|
|
@@ -90,6 +103,13 @@ form.args = {
|
|
|
90
103
|
{ value: '2', total: 3 },
|
|
91
104
|
{ value: '3', total: 3 },
|
|
92
105
|
],
|
|
106
|
+
optionsSearch: [
|
|
107
|
+
{ value: 'Cornichon', label: 'Cornichon' },
|
|
108
|
+
{ value: 'Ail', label: 'Ail' },
|
|
109
|
+
{ value: 'Oignon', label: 'Oignon' },
|
|
110
|
+
{ value: 'Aigre-Doux', label: 'Aigre-douc' },
|
|
111
|
+
{ value: 'Soja sucré', label: 'Soja sucré' },
|
|
112
|
+
],
|
|
93
113
|
cancel: action('cancel'),
|
|
94
114
|
onSelect: action('onSelect'),
|
|
95
115
|
selectOptions: [
|
|
@@ -5,12 +5,12 @@ export const Template = (args) => {
|
|
|
5
5
|
template: hbs`
|
|
6
6
|
<PixCheckbox
|
|
7
7
|
@id={{id}}
|
|
8
|
-
@label={{label}}
|
|
9
8
|
@screenReaderOnly={{screenReaderOnly}}
|
|
10
9
|
@isIndeterminate={{isIndeterminate}}
|
|
11
10
|
@labelSize={{labelSize}}
|
|
12
|
-
@checked={{checked}}
|
|
13
|
-
|
|
11
|
+
@checked={{checked}}>
|
|
12
|
+
{{label}}
|
|
13
|
+
</PixCheckbox>
|
|
14
14
|
`,
|
|
15
15
|
context: args,
|
|
16
16
|
};
|
|
@@ -53,7 +53,6 @@ export const argTypes = {
|
|
|
53
53
|
label: {
|
|
54
54
|
name: 'label',
|
|
55
55
|
description: "Le label de l'input",
|
|
56
|
-
type: { name: 'string', required: false },
|
|
57
56
|
defaultValue: null,
|
|
58
57
|
},
|
|
59
58
|
screenReaderOnly: {
|
|
@@ -38,11 +38,12 @@ La PixCheckbox permet de créer des checkbox basiques ou des checkbox mixées (c
|
|
|
38
38
|
```html
|
|
39
39
|
<PixCheckbox
|
|
40
40
|
@id="superId"
|
|
41
|
-
@label="Recevoir la newsletter"
|
|
42
41
|
@screenReaderOnly={{false}}
|
|
43
42
|
@isIndeterminate={{false}}
|
|
44
43
|
@labelSize="small"
|
|
45
|
-
|
|
44
|
+
>
|
|
45
|
+
Recevoir la newsletter
|
|
46
|
+
</PixCheckBox>
|
|
46
47
|
```
|
|
47
48
|
|
|
48
49
|
## Arguments
|
|
@@ -49,7 +49,7 @@ export const argTypes = {
|
|
|
49
49
|
showModal: {
|
|
50
50
|
name: 'showModal',
|
|
51
51
|
description: "Gérer l'ouverture de la modale",
|
|
52
|
-
type: { name: 'boolean', required:
|
|
52
|
+
type: { name: 'boolean', required: true },
|
|
53
53
|
control: { type: 'boolean' },
|
|
54
54
|
table: {
|
|
55
55
|
type: { summary: 'boolean' },
|
|
@@ -20,14 +20,18 @@ export const multiSelectWithChildComponent = (args) => {
|
|
|
20
20
|
template: hbs`
|
|
21
21
|
<h4>⚠️ La sélection des éléments ne fonctionne pas dans Storybook.</h4>
|
|
22
22
|
<PixMultiSelect
|
|
23
|
-
@
|
|
24
|
-
@
|
|
25
|
-
@
|
|
26
|
-
@
|
|
27
|
-
|
|
28
|
-
@
|
|
23
|
+
@id={{this.id}}
|
|
24
|
+
@label={{this.label}}
|
|
25
|
+
@innerText={{this.titleStars}}
|
|
26
|
+
@screenReaderOnly={{this.screenReaderOnly}}
|
|
27
|
+
|
|
28
|
+
@onSelect={{this.onSelect}}
|
|
29
|
+
@emptyMessage={{this.emptyMessage}}
|
|
30
|
+
|
|
31
|
+
@options={{this.options}} as |star|
|
|
29
32
|
>
|
|
30
33
|
<PixStars
|
|
34
|
+
@alt={{concat "Étoiles " star.value " sur " star.total}}
|
|
31
35
|
@count={{star.value}}
|
|
32
36
|
@total={{star.total}}
|
|
33
37
|
/>
|
|
@@ -39,7 +43,9 @@ export const multiSelectWithChildComponent = (args) => {
|
|
|
39
43
|
|
|
40
44
|
multiSelectWithChildComponent.args = {
|
|
41
45
|
titleStars: 'Sélectionner le niveau souhaité',
|
|
46
|
+
label: 'Résultat évaluation',
|
|
42
47
|
options: [
|
|
48
|
+
{ value: '0', total: 3 },
|
|
43
49
|
{ value: '1', total: 3 },
|
|
44
50
|
{ value: '2', total: 3 },
|
|
45
51
|
{ value: '3', total: 3 },
|
|
@@ -52,17 +58,19 @@ export const multiSelectSearchable = (args) => {
|
|
|
52
58
|
<h4>⚠️ La sélection des éléments ne fonctionne pas dans Storybook.</h4>
|
|
53
59
|
<PixMultiSelect
|
|
54
60
|
style="width:350px"
|
|
55
|
-
@id={{id}}
|
|
56
|
-
@
|
|
57
|
-
@
|
|
58
|
-
@
|
|
59
|
-
|
|
60
|
-
@
|
|
61
|
-
@
|
|
62
|
-
|
|
63
|
-
@
|
|
64
|
-
@selected={{selected}}
|
|
65
|
-
|
|
61
|
+
@id={{this.id}}
|
|
62
|
+
@label={{this.label}}
|
|
63
|
+
@screenReaderOnly={{this.screenReaderOnly}}
|
|
64
|
+
@innerText={{this.innerText}}
|
|
65
|
+
|
|
66
|
+
@isSearchable={{this.isSearchable}}
|
|
67
|
+
@strictSearch={{this.strictSearch}}
|
|
68
|
+
|
|
69
|
+
@onSelect={{this.doSomething}}
|
|
70
|
+
@selected={{this.selected}}
|
|
71
|
+
|
|
72
|
+
@emptyMessage={{this.emptyMessage}}
|
|
73
|
+
@options={{this.options}} as |option|
|
|
66
74
|
>
|
|
67
75
|
{{option.label}}
|
|
68
76
|
</PixMultiSelect>
|
|
@@ -78,17 +86,19 @@ export const multiSelectAsyncOptions = (args) => {
|
|
|
78
86
|
<h4>⚠️ La sélection des éléments ne fonctionne pas dans Storybook.</h4>
|
|
79
87
|
<PixMultiSelect
|
|
80
88
|
style="width:350px"
|
|
81
|
-
@id={{id}}
|
|
82
|
-
@
|
|
83
|
-
@
|
|
84
|
-
@
|
|
85
|
-
|
|
86
|
-
@
|
|
87
|
-
@
|
|
88
|
-
|
|
89
|
-
@
|
|
90
|
-
@selected={{selected}}
|
|
91
|
-
|
|
89
|
+
@id={{this.id}}
|
|
90
|
+
@label={{this.label}}
|
|
91
|
+
@screenReaderOnly={{this.screenReaderOnly}}
|
|
92
|
+
@innerText={{this.innerText}}
|
|
93
|
+
|
|
94
|
+
@isSearchable={{this.isSearchable}}
|
|
95
|
+
@strictSearch={{this.strictSearch}}
|
|
96
|
+
|
|
97
|
+
@onSelect={{this.doSomething}}
|
|
98
|
+
@selected={{this.selected}}
|
|
99
|
+
|
|
100
|
+
@emptyMessage={{this.emptyMessage}}
|
|
101
|
+
@onLoadOptions={{this.onLoadOptions}} as |option|
|
|
92
102
|
>
|
|
93
103
|
{{option.label}}
|
|
94
104
|
</PixMultiSelect>
|
|
@@ -104,20 +114,18 @@ export const argTypes = {
|
|
|
104
114
|
type: { name: 'string', required: true },
|
|
105
115
|
defaultValue: 'aromate',
|
|
106
116
|
},
|
|
107
|
-
|
|
108
|
-
name: '
|
|
109
|
-
description:
|
|
117
|
+
innerText: {
|
|
118
|
+
name: 'innerText',
|
|
119
|
+
description:
|
|
120
|
+
'Donne un titre à sa liste de choix multiple, utilisé comme placeholder lorsque ``isSearchable`` à ``true``',
|
|
110
121
|
type: { name: 'string', required: true },
|
|
111
122
|
defaultValue: 'Rechercher un condiment',
|
|
112
123
|
},
|
|
113
124
|
label: {
|
|
114
125
|
name: 'label',
|
|
115
|
-
description: 'Donne un label au champ
|
|
116
|
-
type: { name: 'string', required:
|
|
117
|
-
|
|
118
|
-
type: { summary: 'string' },
|
|
119
|
-
defaultValue: { summary: null },
|
|
120
|
-
},
|
|
126
|
+
description: 'Donne un label au champ',
|
|
127
|
+
type: { name: 'string', required: true },
|
|
128
|
+
defaultValue: 'Label du champ',
|
|
121
129
|
},
|
|
122
130
|
emptyMessage: {
|
|
123
131
|
name: 'emptyMessage',
|
|
@@ -133,13 +141,6 @@ export const argTypes = {
|
|
|
133
141
|
type: { name: 'string', required: false },
|
|
134
142
|
defaultValue: 'Chargement...',
|
|
135
143
|
},
|
|
136
|
-
placeholder: {
|
|
137
|
-
name: 'placeholder',
|
|
138
|
-
description:
|
|
139
|
-
'Donner une liste d‘exemple pour la recherche utilisateur dans le cas ``isSearchable`` à ``true``',
|
|
140
|
-
type: { name: 'string', required: true },
|
|
141
|
-
defaultValue: 'Curcuma, Thym, ...',
|
|
142
|
-
},
|
|
143
144
|
options: {
|
|
144
145
|
name: 'options',
|
|
145
146
|
description:
|
|
@@ -165,13 +166,6 @@ export const argTypes = {
|
|
|
165
166
|
type: { name: 'array', required: false },
|
|
166
167
|
defaultValue: ['1', '4'],
|
|
167
168
|
},
|
|
168
|
-
showOptionsOnInput: {
|
|
169
|
-
name: 'showOptionsOnInput',
|
|
170
|
-
description:
|
|
171
|
-
'Afficher la liste au focus du champs de saisie lorsque ``isSearchable`` à ``true``',
|
|
172
|
-
type: { name: 'boolean', required: false },
|
|
173
|
-
defaultValue: true,
|
|
174
|
-
},
|
|
175
169
|
isSearchable: {
|
|
176
170
|
name: 'isSearchable',
|
|
177
171
|
description: 'Permet de rajouter une saisie utilisateur pour faciliter la recherche',
|
|
@@ -185,15 +179,10 @@ export const argTypes = {
|
|
|
185
179
|
type: { name: 'boolean', required: false },
|
|
186
180
|
defaultValue: false,
|
|
187
181
|
},
|
|
188
|
-
|
|
189
|
-
name: '
|
|
190
|
-
description:
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
type: { name: 'string', required: false },
|
|
194
|
-
table: {
|
|
195
|
-
type: { summary: 'string' },
|
|
196
|
-
defaultValue: { summary: 'small' },
|
|
197
|
-
},
|
|
182
|
+
screenReaderOnly: {
|
|
183
|
+
name: 'screenReaderOnly',
|
|
184
|
+
description: "Permet de cacher à l'écran le label tout en restant vocalisable",
|
|
185
|
+
type: { name: 'boolean', required: false },
|
|
186
|
+
defaultValue: false,
|
|
198
187
|
},
|
|
199
188
|
};
|
|
@@ -40,11 +40,14 @@ Il est possible de donner un message via `loadingMessage` à afficher à la plac
|
|
|
40
40
|
|
|
41
41
|
```html
|
|
42
42
|
<PixMultiSelect
|
|
43
|
-
@title={{title}}
|
|
44
|
-
@emptyMessage={{emptyMessage}}
|
|
45
43
|
@id={{id}}
|
|
44
|
+
@label={{label}}
|
|
45
|
+
@screenReaderOnly={{screenReaderOnly}}
|
|
46
|
+
@innerText={{innerText}}
|
|
47
|
+
|
|
46
48
|
@onSelect={{doSomething}}
|
|
47
49
|
@selected={{selected}}
|
|
50
|
+
@emptyMessage={{emptyMessage}}
|
|
48
51
|
@options={{options}} as |option|>
|
|
49
52
|
{{option.label}}
|
|
50
53
|
</PixMultiSelect>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@1024pix/pix-ui",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "19.0.1",
|
|
4
4
|
"description": "Pix-UI is the implementation of Pix design principles and guidelines for its products.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ember-addon"
|
|
@@ -72,6 +72,7 @@
|
|
|
72
72
|
"@storybook/ember-cli-storybook": "^0.4.0",
|
|
73
73
|
"@storybook/storybook-deployer": "^2.8.10",
|
|
74
74
|
"@storybook/theming": "^6.3.7",
|
|
75
|
+
"@testing-library/user-event": "^14.4.3",
|
|
75
76
|
"axios": "^0.21.1",
|
|
76
77
|
"babel-eslint": "^10.1.0",
|
|
77
78
|
"babel-plugin-ember-modules-api-polyfill": "^2.13.4",
|