@1024pix/pix-ui 55.18.1 → 55.19.0

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.
@@ -0,0 +1,85 @@
1
+ import { on } from '@ember/modifier';
2
+ import { action } from '@ember/object';
3
+ import Component from '@glimmer/component';
4
+
5
+ import PixButton from './pix-button';
6
+ import PixIcon from './pix-icon';
7
+
8
+ export default class PixFilterBanner extends Component {
9
+ get displayTitle() {
10
+ return Boolean(this.args.title);
11
+ }
12
+
13
+ get displayDetails() {
14
+ return Boolean(this.args.details);
15
+ }
16
+
17
+ get displayClearFilters() {
18
+ return Boolean(this.args.clearFiltersLabel);
19
+ }
20
+
21
+ get displayLoadFilters() {
22
+ return Boolean(this.args.loadFiltersLabel);
23
+ }
24
+
25
+ get displayActionMenu() {
26
+ return this.displayClearFilters || this.displayDetails || this.displayLoadFilters;
27
+ }
28
+
29
+ @action
30
+ onSubmit(event) {
31
+ if (this.args.onLoadFilters) {
32
+ event.preventDefault();
33
+ this.args.onLoadFilters(event);
34
+ }
35
+ }
36
+
37
+ <template>
38
+ <form {{on "submit" this.onSubmit}} class="pix-filter-banner" ...attributes>
39
+ {{#if this.displayTitle}}
40
+ <p class="pix-filter-banner__title">
41
+ <PixIcon
42
+ @name="filter"
43
+ @plainIcon={{true}}
44
+ class="pix-filter-banner__icon-title"
45
+ aria-hidden="true"
46
+ />
47
+ {{@title}}
48
+ </p>
49
+ {{/if}}
50
+
51
+ <div class="pix-filter-banner__container">
52
+ <div class="pix-filter-banner__filter">
53
+ {{yield}}
54
+ </div>
55
+
56
+ {{#if this.displayActionMenu}}
57
+ <div class="pix-filter-banner__action">
58
+ {{#if this.displayDetails}}
59
+ <p>{{@details}}</p>
60
+ {{/if}}
61
+
62
+ {{#if this.displayLoadFilters}}
63
+ <PixButton @variant="primary" @type="submit" @size="small">
64
+ {{@loadFiltersLabel}}
65
+ </PixButton>
66
+ {{/if}}
67
+
68
+ {{#if this.displayClearFilters}}
69
+ <PixButton
70
+ class="pix-filter-banner__button"
71
+ @iconBefore="delete"
72
+ @variant="tertiary"
73
+ @size="small"
74
+ @triggerAction={{@onClearFilters}}
75
+ @isDisabled={{@isClearFilterButtonDisabled}}
76
+ >
77
+ {{@clearFiltersLabel}}
78
+ </PixButton>
79
+ {{/if}}
80
+ </div>
81
+ {{/if}}
82
+ </div>
83
+ </form>
84
+ </template>
85
+ }
@@ -60,7 +60,9 @@
60
60
  />
61
61
  {{/if}}
62
62
 
63
- {{option.label}}
63
+ <span class="pix-select-list-category__option-label">
64
+ {{option.label}}
65
+ </span>
64
66
 
65
67
  {{#if (eq option.value @value)}}
66
68
  <PixIcon
@@ -97,7 +99,9 @@
97
99
  />
98
100
  {{/if}}
99
101
 
100
- {{option.label}}
102
+ <span class="pix-select-list-category__option-label">
103
+ {{option.label}}
104
+ </span>
101
105
 
102
106
  {{#if (eq option.value @value)}}
103
107
  <PixIcon
@@ -0,0 +1,235 @@
1
+ import { on } from '@ember/modifier';
2
+ import { action } from '@ember/object';
3
+ import { guidFor } from '@ember/object/internals';
4
+ import { inject as service } from '@ember/service';
5
+ import Component from '@glimmer/component';
6
+ import { tracked } from '@glimmer/tracking';
7
+ import onClickOutsideModifier from 'ember-click-outside/modifiers/on-click-outside';
8
+ import { FloatingUI } from 'ember-primitives/floating-ui';
9
+
10
+ import onArrowDownUpAction from '../modifiers/on-arrow-down-up-action';
11
+ import onEscapeAction from '../modifiers/on-escape-action';
12
+ import PixIcon from './pix-icon';
13
+ import PixLabel from './pix-label';
14
+ import PixSelectList from './pix-select-list';
15
+
16
+ export default class PixSelect extends Component {
17
+ @service elementHelper;
18
+ @tracked isExpanded = false;
19
+ @tracked searchValue = null;
20
+
21
+ constructor(...args) {
22
+ super(...args);
23
+
24
+ this.searchId = 'search-input-' + guidFor(this);
25
+ this.selectId = this.args.id ? this.args.id : 'select-' + guidFor(this);
26
+ this.listId = `listbox-${this.selectId}`;
27
+
28
+ if (!this.args.isComputeWidthDisabled) {
29
+ this.elementHelper.waitForElement(this.listId).then((elementList) => {
30
+ const baseFontRemRatio = Number(
31
+ getComputedStyle(document.querySelector('html')).fontSize.match(/\d+(\.\d+)?/)[0],
32
+ );
33
+
34
+ const listWidth = elementList.parentNode.getBoundingClientRect().width;
35
+ const selectWidth = listWidth / baseFontRemRatio;
36
+
37
+ const element = document.getElementById(`container-${this.selectId}`);
38
+ element.style.setProperty('--pix-select-width', `${selectWidth}rem`);
39
+ });
40
+ }
41
+ }
42
+
43
+ get displayDefaultOption() {
44
+ return !this.searchValue && !this.args.hideDefaultOption;
45
+ }
46
+
47
+ get className() {
48
+ const classes = ['pix-select-button'];
49
+ if (this.args.className) {
50
+ classes.push(this.args.className);
51
+ }
52
+ if (this.args.errorMessage) {
53
+ classes.push('pix-select-button--error');
54
+ }
55
+
56
+ return classes.join(' ');
57
+ }
58
+
59
+ get isAriaExpanded() {
60
+ return this.isExpanded ? 'true' : 'false';
61
+ }
62
+
63
+ get placeholder() {
64
+ if (!this.args.value) return this.args.placeholder;
65
+ const option = this.args.options.find((option) => option.value === this.args.value);
66
+ return option ? option.label : this.args.placeholder;
67
+ }
68
+
69
+ get defaultOption() {
70
+ return {
71
+ value: '',
72
+ };
73
+ }
74
+
75
+ @action
76
+ toggleDropdown(event) {
77
+ if (this.isExpanded) {
78
+ this.hideDropdown(event);
79
+ } else {
80
+ this.showDropdown(event);
81
+ }
82
+ }
83
+
84
+ @action
85
+ showDropdown(event) {
86
+ event.preventDefault();
87
+ if (this.args.isDisabled) return;
88
+
89
+ this.isExpanded = true;
90
+ }
91
+
92
+ @action
93
+ hideDropdown(event) {
94
+ if (this.isExpanded) {
95
+ event.preventDefault();
96
+
97
+ this.isExpanded = false;
98
+ }
99
+ }
100
+
101
+ @action
102
+ onChange(option, event) {
103
+ if (this.args.isDisabled) return;
104
+
105
+ this.args.onChange(option.value);
106
+
107
+ this.hideDropdown(event);
108
+ document.getElementById(this.selectId).focus();
109
+ }
110
+
111
+ @action
112
+ setSearchValue(event) {
113
+ this.searchValue = event.target.value.trim();
114
+ }
115
+
116
+ @action
117
+ lockTab(event) {
118
+ if (event.code === 'Tab' && this.isExpanded) {
119
+ event.preventDefault();
120
+ if (this.args.isSearchable) document.getElementById(this.searchId).focus();
121
+ }
122
+ }
123
+
124
+ @action
125
+ focus(event) {
126
+ if (!event.target) return;
127
+ if (!this.isExpanded) return;
128
+
129
+ if (this.args.value) {
130
+ event.target.querySelector("[aria-selected='true']")?.focus();
131
+ } else if (this.args.isSearchable) {
132
+ event.target.querySelector(`#${this.searchId}`)?.focus();
133
+ } else if (this.displayDefaultOption) {
134
+ event.target.querySelector("[aria-selected='true']")?.focus();
135
+ }
136
+ }
137
+
138
+ <template>
139
+ <div
140
+ class="pix-select {{if @inlineLabel ' pix-select--inline'}}"
141
+ id="container-{{this.selectId}}"
142
+ {{onClickOutsideModifier this.hideDropdown}}
143
+ {{onArrowDownUpAction this.listId this.showDropdown this.isExpanded}}
144
+ {{onEscapeAction this.hideDropdown this.selectId}}
145
+ {{on "keydown" this.lockTab}}
146
+ ...attributes
147
+ >
148
+ {{#if (has-block "label")}}
149
+ <PixLabel
150
+ @for={{this.selectId}}
151
+ @requiredLabel={{@requiredLabel}}
152
+ @subLabel={{@subLabel}}
153
+ @size={{@size}}
154
+ @screenReaderOnly={{@screenReaderOnly}}
155
+ @inlineLabel={{@inlineLabel}}
156
+ >
157
+ {{yield to="label"}}
158
+ </PixLabel>
159
+ {{/if}}
160
+
161
+ <FloatingUI @placement="bottom" as |reference floating|>
162
+ <div>
163
+ <button
164
+ {{reference}}
165
+ type="button"
166
+ id={{this.selectId}}
167
+ class={{this.className}}
168
+ {{on "click" this.toggleDropdown}}
169
+ aria-expanded={{this.isAriaExpanded}}
170
+ aria-controls={{this.listId}}
171
+ aria-disabled={{@isDisabled}}
172
+ >
173
+ {{#if @iconName}}
174
+ <PixIcon
175
+ @name={{@iconName}}
176
+ @plainIcon={{@plainIcon}}
177
+ @ariaHidden={{true}}
178
+ class="pix-select-button__icon"
179
+ />
180
+ {{/if}}
181
+
182
+ <span class="pix-select-button__text">{{this.placeholder}}</span>
183
+
184
+ <PixIcon
185
+ class="pix-select-button__dropdown-icon"
186
+ @ariaHidden={{true}}
187
+ @name={{if this.isExpanded "chevronTop" "chevronBottom"}}
188
+ />
189
+ </button>
190
+
191
+ <div class="pix-select__floating-container" {{floating}}>
192
+ <div
193
+ class="pix-select__dropdown
194
+ {{unless this.isExpanded ' pix-select__dropdown--closed'}}"
195
+ {{on "transitionend" this.focus}}
196
+ >
197
+ {{#if @isSearchable}}
198
+ <div class="pix-select__search">
199
+ <PixIcon class="pix-select-search__icon" @name="search" @ariaHidden={{true}} />
200
+ <label class="screen-reader-only" for={{this.searchId}}>{{@searchLabel}}</label>
201
+ <input
202
+ class="pix-select-search__input"
203
+ id={{this.searchId}}
204
+ autocomplete="off"
205
+ tabindex={{if this.isExpanded "0" "-1"}}
206
+ placeholder={{@searchPlaceholder}}
207
+ {{on "input" this.setSearchValue}}
208
+ />
209
+ </div>
210
+ {{/if}}
211
+ <PixSelectList
212
+ @hideDefaultOption={{@hideDefaultOption}}
213
+ @listId={{this.listId}}
214
+ @value={{@value}}
215
+ @displayDefaultOption={{this.displayDefaultOption}}
216
+ @searchValue={{this.searchValue}}
217
+ @onChange={{this.onChange}}
218
+ @defaultOption={{this.defaultOption}}
219
+ @selectId={{this.selectId}}
220
+ @isExpanded={{this.isExpanded}}
221
+ @options={{@options}}
222
+ @defaultOptionValue={{@placeholder}}
223
+ @emptySearchMessage={{@emptySearchMessage}}
224
+ />
225
+ </div>
226
+ </div>
227
+
228
+ {{#if @errorMessage}}
229
+ <p class="pix-select__error-message">{{@errorMessage}}</p>
230
+ {{/if}}
231
+ </div>
232
+ </FloatingUI>
233
+ </div>
234
+ </template>
235
+ }
@@ -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(event);
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 = null, 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
+ if (callback) callback(event);
21
+ }
22
+
23
+ element.addEventListener('keydown', handleKeyUp);
24
+
25
+ return () => {
26
+ element.removeEventListener('keydown', handleKeyUp);
27
+ };
28
+ });
@@ -0,0 +1,23 @@
1
+ import { modifier } from 'ember-modifier';
2
+
3
+ export default modifier((element, [callback, focusId = null]) => {
4
+ function handleKeyUp(event) {
5
+ const ESCAPE_KEY = 'Escape';
6
+
7
+ if (event.key !== ESCAPE_KEY) {
8
+ return;
9
+ }
10
+
11
+ callback(event);
12
+
13
+ if (focusId) {
14
+ document.getElementById(focusId).focus();
15
+ }
16
+ }
17
+
18
+ element.addEventListener('keyup', handleKeyUp);
19
+
20
+ return () => {
21
+ element.removeEventListener('keyup', handleKeyUp);
22
+ };
23
+ });
@@ -0,0 +1,20 @@
1
+ import { modifier } from 'ember-modifier';
2
+
3
+ export default modifier((element, [callback]) => {
4
+ const listener = (event) => handleKeyUp(event, callback);
5
+ element.addEventListener('keydown', listener);
6
+
7
+ return () => {
8
+ element.removeEventListener('keydown', listener);
9
+ };
10
+ });
11
+
12
+ function handleKeyUp(event, callback) {
13
+ const SPACE_KEY = ' ';
14
+
15
+ if (event.key !== SPACE_KEY) {
16
+ return;
17
+ }
18
+
19
+ callback(event);
20
+ }
@@ -0,0 +1,12 @@
1
+ import { modifier } from 'ember-modifier';
2
+
3
+ export default modifier(function onWindowResize(element, [action]) {
4
+ const actionWithElement = () => action(element);
5
+
6
+ window.addEventListener('resize', actionWithElement);
7
+ actionWithElement();
8
+
9
+ return () => {
10
+ window.removeEventListener('resize', actionWithElement);
11
+ };
12
+ });
@@ -0,0 +1,117 @@
1
+ import { modifier } from 'ember-modifier';
2
+
3
+ let sourceActiveElement = null;
4
+
5
+ export default modifier(function trapFocus(element, [isOpen, focusOnClose = true]) {
6
+ const [firstFocusableElement] = findFocusableElements(element);
7
+
8
+ if (isOpen) {
9
+ preventPageScrolling();
10
+ sourceActiveElement = document.activeElement;
11
+ focusElement(firstFocusableElement, element);
12
+ } else if (sourceActiveElement) {
13
+ allowPageScrolling();
14
+
15
+ if (focusOnClose) {
16
+ focusElement(sourceActiveElement, element);
17
+ }
18
+ sourceActiveElement = null;
19
+ }
20
+
21
+ element.addEventListener('keydown', (event) => {
22
+ handleKeyDown(event, element);
23
+ });
24
+
25
+ return () => {
26
+ element.removeEventListener('keydown', (event) => {
27
+ handleKeyDown(event, element);
28
+ });
29
+ allowPageScrolling();
30
+ };
31
+ });
32
+
33
+ function findFocusableElements(element) {
34
+ return [
35
+ ...element.querySelectorAll(
36
+ 'a[href], button, input, textarea, select, details,[tabindex]:not([tabindex="-1"])',
37
+ ),
38
+ ].filter((el) => !el.hasAttribute('disabled') && !el.getAttribute('aria-hidden'));
39
+ }
40
+
41
+ function focusElement(elementToFocus, element) {
42
+ let focusOnce = false;
43
+
44
+ const handleTransitionEnd = () => {
45
+ if (!focusOnce) {
46
+ elementToFocus.focus();
47
+ focusOnce = true;
48
+ }
49
+ };
50
+
51
+ if (hasTransitionDuration(element)) {
52
+ element.addEventListener('transitionend', handleTransitionEnd);
53
+ } else if (hasAnimationDuration(element)) {
54
+ element.addEventListener('animationend', handleTransitionEnd);
55
+ } else {
56
+ elementToFocus.focus();
57
+ }
58
+
59
+ return () => {
60
+ if (hasTransitionDuration(element)) {
61
+ element.removeEventListener('transitionend', handleTransitionEnd);
62
+ } else if (hasAnimationDuration(element)) {
63
+ element.removeEventListener('animationend', handleTransitionEnd);
64
+ }
65
+ };
66
+ }
67
+
68
+ function preventPageScrolling() {
69
+ document.body.classList.add('body__trap-focus');
70
+ }
71
+
72
+ function allowPageScrolling() {
73
+ document.body.classList.remove('body__trap-focus');
74
+ }
75
+
76
+ function hasTransitionDuration(element) {
77
+ return hasDurationByKey(element, 'transition-duration');
78
+ }
79
+
80
+ function hasAnimationDuration(element) {
81
+ return hasDurationByKey(element, 'animation-duration');
82
+ }
83
+
84
+ function hasDurationByKey(element, key) {
85
+ return !['', '0s'].includes(getComputedStyle(element, null).getPropertyValue(key));
86
+ }
87
+
88
+ function handleKeyDown(event, element) {
89
+ const TAB_KEY = 'Tab';
90
+ const focusableElements = findFocusableElements(element);
91
+ const [firstFocusableElement] = focusableElements;
92
+ const lastFocusableElement = focusableElements[focusableElements.length - 1];
93
+
94
+ if (event.key !== TAB_KEY) {
95
+ return;
96
+ }
97
+
98
+ const handleBackwardTab = () => {
99
+ if (document.activeElement === firstFocusableElement) {
100
+ event.preventDefault();
101
+ lastFocusableElement.focus();
102
+ }
103
+ };
104
+
105
+ const handleForwardTab = () => {
106
+ if (document.activeElement === lastFocusableElement) {
107
+ event.preventDefault();
108
+ firstFocusableElement.focus();
109
+ }
110
+ };
111
+
112
+ if (event.shiftKey) {
113
+ handleBackwardTab();
114
+ } else {
115
+ handleForwardTab();
116
+ }
117
+ }
@@ -34,17 +34,13 @@
34
34
  &__action {
35
35
  display: flex;
36
36
  flex-direction: column;
37
- align-items: flex-end;
38
- }
39
-
40
- &__details {
41
- padding: 0;
37
+ gap: var(--pix-spacing-1x);
38
+ align-items: center;
42
39
  font-weight: var(--pix-font-medium);
43
40
  }
44
41
 
45
42
  &__button {
46
43
  padding: 0;
47
- font-size: 0.875rem;
48
44
  }
49
45
  }
50
46
 
@@ -56,6 +56,11 @@
56
56
  }
57
57
  }
58
58
 
59
+ &__option-label {
60
+ overflow: hidden;
61
+ text-overflow: ellipsis;
62
+ }
63
+
59
64
  &__option-checked {
60
65
  position: absolute;
61
66
  top: 50%;
@@ -8,6 +8,7 @@
8
8
  flex-direction: column;
9
9
  gap: var(--pix-spacing-1x);
10
10
  width: var(--pix-select-width);
11
+ min-width: fit-content;
11
12
  max-width: 100%;
12
13
 
13
14
  &--inline {
@@ -16,13 +17,15 @@
16
17
  align-items: center;
17
18
  }
18
19
 
20
+ &__floating-container {
21
+ z-index: 200;
22
+ width: min(var(--pix-select-width), 100%);
23
+ }
24
+
19
25
  &__dropdown {
20
26
  @extend %pix-shadow-xs;
21
27
 
22
- position: absolute;
23
- z-index: 200;
24
28
  width: 100%;
25
- min-width: fit-content;
26
29
  max-height: 12rem;
27
30
  margin-top: var(--pix-spacing-1x);
28
31
  padding: 0;
@@ -32,7 +35,7 @@
32
35
  background-color: var(--pix-neutral-0);
33
36
  border-top: none;
34
37
  border-radius: 0 0 var(--pix-spacing-1x) var(--pix-spacing-1x);
35
- transition: all 0.1s ease-in-out;
38
+ transition: visibility 0.2s ease-in-out, opacity 0.2s ease-in-out, max-height 0s;
36
39
 
37
40
  &::-webkit-scrollbar {
38
41
  width: 0.5rem;
@@ -55,8 +58,10 @@
55
58
  }
56
59
 
57
60
  &--closed {
61
+ max-height: 0;
58
62
  visibility: hidden;
59
63
  opacity: 0;
64
+ transition: visibility 0.2s ease-in-out, opacity 0.2s ease-in-out, max-height 0s 0.2s;
60
65
  }
61
66
  }
62
67
 
@@ -1,82 +1 @@
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(event);
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
- }
1
+ export { default } from '@1024pix/pix-ui/modifiers/on-arrow-down-up-action';
@@ -1,28 +1 @@
1
- import { modifier } from 'ember-modifier';
2
-
3
- export default modifier((element, [callback = null, 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
- if (callback) callback(event);
21
- }
22
-
23
- element.addEventListener('keydown', handleKeyUp);
24
-
25
- return () => {
26
- element.removeEventListener('keydown', handleKeyUp);
27
- };
28
- });
1
+ export { default } from '@1024pix/pix-ui/modifiers/on-enter-action';
@@ -1,23 +1 @@
1
- import { modifier } from 'ember-modifier';
2
-
3
- export default modifier((element, [callback, focusId = null]) => {
4
- function handleKeyUp(event) {
5
- const ESCAPE_KEY = 'Escape';
6
-
7
- if (event.key !== ESCAPE_KEY) {
8
- return;
9
- }
10
-
11
- callback(event);
12
-
13
- if (focusId) {
14
- document.getElementById(focusId).focus();
15
- }
16
- }
17
-
18
- element.addEventListener('keyup', handleKeyUp);
19
-
20
- return () => {
21
- element.removeEventListener('keyup', handleKeyUp);
22
- };
23
- });
1
+ export { default } from '@1024pix/pix-ui/modifiers/on-escape-action';
@@ -1,20 +1 @@
1
- import { modifier } from 'ember-modifier';
2
-
3
- export default modifier((element, [callback]) => {
4
- const listener = (event) => handleKeyUp(event, callback);
5
- element.addEventListener('keydown', listener);
6
-
7
- return () => {
8
- element.removeEventListener('keydown', listener);
9
- };
10
- });
11
-
12
- function handleKeyUp(event, callback) {
13
- const SPACE_KEY = ' ';
14
-
15
- if (event.key !== SPACE_KEY) {
16
- return;
17
- }
18
-
19
- callback(event);
20
- }
1
+ export { default } from '@1024pix/pix-ui/modifiers/on-space-action';
@@ -1,12 +1 @@
1
- import { modifier } from 'ember-modifier';
2
-
3
- export default modifier(function onWindowResize(element, [action]) {
4
- const actionWithElement = () => action(element);
5
-
6
- window.addEventListener('resize', actionWithElement);
7
- actionWithElement();
8
-
9
- return () => {
10
- window.removeEventListener('resize', actionWithElement);
11
- };
12
- });
1
+ export { default } from '@1024pix/pix-ui/modifiers/on-window-resize';
@@ -1,117 +1 @@
1
- import { modifier } from 'ember-modifier';
2
-
3
- let sourceActiveElement = null;
4
-
5
- export default modifier(function trapFocus(element, [isOpen, focusOnClose = true]) {
6
- const [firstFocusableElement] = findFocusableElements(element);
7
-
8
- if (isOpen) {
9
- preventPageScrolling();
10
- sourceActiveElement = document.activeElement;
11
- focusElement(firstFocusableElement, element);
12
- } else if (sourceActiveElement) {
13
- allowPageScrolling();
14
-
15
- if (focusOnClose) {
16
- focusElement(sourceActiveElement, element);
17
- }
18
- sourceActiveElement = null;
19
- }
20
-
21
- element.addEventListener('keydown', (event) => {
22
- handleKeyDown(event, element);
23
- });
24
-
25
- return () => {
26
- element.removeEventListener('keydown', (event) => {
27
- handleKeyDown(event, element);
28
- });
29
- allowPageScrolling();
30
- };
31
- });
32
-
33
- function findFocusableElements(element) {
34
- return [
35
- ...element.querySelectorAll(
36
- 'a[href], button, input, textarea, select, details,[tabindex]:not([tabindex="-1"])',
37
- ),
38
- ].filter((el) => !el.hasAttribute('disabled') && !el.getAttribute('aria-hidden'));
39
- }
40
-
41
- function focusElement(elementToFocus, element) {
42
- let focusOnce = false;
43
-
44
- const handleTransitionEnd = () => {
45
- if (!focusOnce) {
46
- elementToFocus.focus();
47
- focusOnce = true;
48
- }
49
- };
50
-
51
- if (hasTransitionDuration(element)) {
52
- element.addEventListener('transitionend', handleTransitionEnd);
53
- } else if (hasAnimationDuration(element)) {
54
- element.addEventListener('animationend', handleTransitionEnd);
55
- } else {
56
- elementToFocus.focus();
57
- }
58
-
59
- return () => {
60
- if (hasTransitionDuration(element)) {
61
- element.removeEventListener('transitionend', handleTransitionEnd);
62
- } else if (hasAnimationDuration(element)) {
63
- element.removeEventListener('animationend', handleTransitionEnd);
64
- }
65
- };
66
- }
67
-
68
- function preventPageScrolling() {
69
- document.body.classList.add('body__trap-focus');
70
- }
71
-
72
- function allowPageScrolling() {
73
- document.body.classList.remove('body__trap-focus');
74
- }
75
-
76
- function hasTransitionDuration(element) {
77
- return hasDurationByKey(element, 'transition-duration');
78
- }
79
-
80
- function hasAnimationDuration(element) {
81
- return hasDurationByKey(element, 'animation-duration');
82
- }
83
-
84
- function hasDurationByKey(element, key) {
85
- return !['', '0s'].includes(getComputedStyle(element, null).getPropertyValue(key));
86
- }
87
-
88
- function handleKeyDown(event, element) {
89
- const TAB_KEY = 'Tab';
90
- const focusableElements = findFocusableElements(element);
91
- const [firstFocusableElement] = focusableElements;
92
- const lastFocusableElement = focusableElements[focusableElements.length - 1];
93
-
94
- if (event.key !== TAB_KEY) {
95
- return;
96
- }
97
-
98
- const handleBackwardTab = () => {
99
- if (document.activeElement === firstFocusableElement) {
100
- event.preventDefault();
101
- lastFocusableElement.focus();
102
- }
103
- };
104
-
105
- const handleForwardTab = () => {
106
- if (document.activeElement === lastFocusableElement) {
107
- event.preventDefault();
108
- firstFocusableElement.focus();
109
- }
110
- };
111
-
112
- if (event.shiftKey) {
113
- handleBackwardTab();
114
- } else {
115
- handleForwardTab();
116
- }
117
- }
1
+ export { default } from '@1024pix/pix-ui/modifiers/trap-focus';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1024pix/pix-ui",
3
- "version": "55.18.1",
3
+ "version": "55.19.0",
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
  "ember-lifeline": "^7.0.0",
73
73
  "ember-modifier": "^4.2.0",
74
74
  "ember-popperjs": "^3.0.0",
75
+ "ember-primitives": "^0.29.0",
75
76
  "ember-template-imports": "^4.3.0",
76
77
  "ember-truth-helpers": "^4.0.0"
77
78
  },
@@ -1,42 +0,0 @@
1
- <div class="pix-filter-banner" ...attributes>
2
- {{#if this.displayTitle}}
3
- <p class="pix-filter-banner__title">
4
- <PixIcon
5
- @name="filter"
6
- @plainIcon={{true}}
7
- class="pix-filter-banner__icon-title"
8
- aria-hidden="true"
9
- />
10
- {{@title}}
11
- </p>
12
- {{/if}}
13
-
14
- <div class="pix-filter-banner__container">
15
- <div class="pix-filter-banner__filter">
16
- {{yield}}
17
- </div>
18
-
19
- {{#if this.displayActionMenu}}
20
- <div class="pix-filter-banner__action">
21
- {{#if this.displayDetails}}
22
- <p class="pix-filter-banner__details">{{@details}}</p>
23
- {{/if}}
24
-
25
- {{#if this.displayClearFilters}}
26
- <div class="pix-filter-banner__button-container">
27
- <PixButton
28
- @iconBefore="delete"
29
- class="pix-filter-banner__button"
30
- @variant="tertiary"
31
- @size="small"
32
- @triggerAction={{@onClearFilters}}
33
- @isDisabled={{@isClearFilterButtonDisabled}}
34
- >
35
- {{@clearFiltersLabel}}
36
- </PixButton>
37
- </div>
38
- {{/if}}
39
- </div>
40
- {{/if}}
41
- </div>
42
- </div>
@@ -1,19 +0,0 @@
1
- import Component from '@glimmer/component';
2
-
3
- export default class PixFilterBanner extends Component {
4
- get displayTitle() {
5
- return Boolean(this.args.title);
6
- }
7
-
8
- get displayDetails() {
9
- return Boolean(this.args.details);
10
- }
11
-
12
- get displayClearFilters() {
13
- return Boolean(this.args.clearFiltersLabel);
14
- }
15
-
16
- get displayActionMenu() {
17
- return this.displayClearFilters || this.displayDetails;
18
- }
19
- }
@@ -1,91 +0,0 @@
1
- <div
2
- class="pix-select {{if @inlineLabel ' pix-select--inline'}}"
3
- id="container-{{this.selectId}}"
4
- {{on-click-outside this.hideDropdown}}
5
- {{on-arrow-down-up-action this.listId this.showDropdown this.isExpanded}}
6
- {{on-escape-action this.hideDropdown this.selectId}}
7
- {{on "keydown" this.lockTab}}
8
- ...attributes
9
- >
10
- {{#if (has-block "label")}}
11
- <PixLabel
12
- @for={{this.selectId}}
13
- @requiredLabel={{@requiredLabel}}
14
- @subLabel={{@subLabel}}
15
- @size={{@size}}
16
- @screenReaderOnly={{@screenReaderOnly}}
17
- @inlineLabel={{@inlineLabel}}
18
- >
19
- {{yield to="label"}}
20
- </PixLabel>
21
- {{/if}}
22
-
23
- <div>
24
- <PopperJS @placement={{or @placement "bottom-start"}} as |reference popover|>
25
- <button
26
- {{reference}}
27
- type="button"
28
- id={{this.selectId}}
29
- class={{this.className}}
30
- {{on "click" this.toggleDropdown}}
31
- aria-expanded={{this.isAriaExpanded}}
32
- aria-controls={{this.listId}}
33
- aria-disabled={{@isDisabled}}
34
- >
35
- {{#if @iconName}}
36
- <PixIcon
37
- @name={{@iconName}}
38
- @plainIcon={{@plainIcon}}
39
- @ariaHidden={{true}}
40
- class="pix-select-button__icon"
41
- />
42
- {{/if}}
43
-
44
- <span class="pix-select-button__text">{{this.placeholder}}</span>
45
-
46
- <PixIcon
47
- class="pix-select-button__dropdown-icon"
48
- @ariaHidden={{true}}
49
- @name={{if this.isExpanded "chevronTop" "chevronBottom"}}
50
- />
51
- </button>
52
- <div
53
- {{popover}}
54
- class="pix-select__dropdown {{unless this.isExpanded ' pix-select__dropdown--closed'}}"
55
- {{on "transitionend" this.focus}}
56
- >
57
- {{#if @isSearchable}}
58
- <div class="pix-select__search">
59
- <PixIcon class="pix-select-search__icon" @name="search" @ariaHidden={{true}} />
60
- <label class="screen-reader-only" for={{this.searchId}}>{{@searchLabel}}</label>
61
- <input
62
- class="pix-select-search__input"
63
- id={{this.searchId}}
64
- autocomplete="off"
65
- tabindex={{if this.isExpanded "0" "-1"}}
66
- placeholder={{@searchPlaceholder}}
67
- {{on "input" this.setSearchValue}}
68
- />
69
- </div>
70
- {{/if}}
71
- <PixSelectList
72
- @hideDefaultOption={{@hideDefaultOption}}
73
- @listId={{this.listId}}
74
- @value={{@value}}
75
- @displayDefaultOption={{this.displayDefaultOption}}
76
- @searchValue={{this.searchValue}}
77
- @onChange={{this.onChange}}
78
- @defaultOption={{this.defaultOption}}
79
- @selectId={{this.selectId}}
80
- @isExpanded={{this.isExpanded}}
81
- @options={{@options}}
82
- @defaultOptionValue={{@placeholder}}
83
- @emptySearchMessage={{@emptySearchMessage}}
84
- />
85
- </div>
86
- </PopperJS>
87
- {{#if @errorMessage}}
88
- <p class="pix-select__error-message">{{@errorMessage}}</p>
89
- {{/if}}
90
- </div>
91
- </div>
@@ -1,127 +0,0 @@
1
- import { action } from '@ember/object';
2
- import { guidFor } from '@ember/object/internals';
3
- import { inject as service } from '@ember/service';
4
- import Component from '@glimmer/component';
5
- import { tracked } from '@glimmer/tracking';
6
-
7
- export default class PixSelect extends Component {
8
- @service elementHelper;
9
- @tracked isExpanded = false;
10
- @tracked searchValue = null;
11
-
12
- constructor(...args) {
13
- super(...args);
14
-
15
- this.searchId = 'search-input-' + guidFor(this);
16
- this.selectId = this.args.id ? this.args.id : 'select-' + guidFor(this);
17
- this.listId = `listbox-${this.selectId}`;
18
-
19
- if (!this.args.isComputeWidthDisabled) {
20
- this.elementHelper.waitForElement(this.listId).then((elementList) => {
21
- const baseFontRemRatio = Number(
22
- getComputedStyle(document.querySelector('html')).fontSize.match(/\d+(\.\d+)?/)[0],
23
- );
24
- const listWidth = elementList.getBoundingClientRect().width;
25
- const selectWidth = listWidth / baseFontRemRatio;
26
-
27
- const element = document.getElementById(`container-${this.selectId}`);
28
- element.style.setProperty('--pix-select-width', `${selectWidth}rem`);
29
- });
30
- }
31
- }
32
-
33
- get displayDefaultOption() {
34
- return !this.searchValue && !this.args.hideDefaultOption;
35
- }
36
-
37
- get className() {
38
- const classes = ['pix-select-button'];
39
- if (this.args.className) {
40
- classes.push(this.args.className);
41
- }
42
- if (this.args.errorMessage) {
43
- classes.push('pix-select-button--error');
44
- }
45
-
46
- return classes.join(' ');
47
- }
48
-
49
- get isAriaExpanded() {
50
- return this.isExpanded ? 'true' : 'false';
51
- }
52
-
53
- get placeholder() {
54
- if (!this.args.value) return this.args.placeholder;
55
- const option = this.args.options.find((option) => option.value === this.args.value);
56
- return option ? option.label : this.args.placeholder;
57
- }
58
-
59
- get defaultOption() {
60
- return {
61
- value: '',
62
- };
63
- }
64
-
65
- @action
66
- toggleDropdown(event) {
67
- if (this.isExpanded) {
68
- this.hideDropdown(event);
69
- } else {
70
- this.showDropdown(event);
71
- }
72
- }
73
-
74
- @action
75
- showDropdown(event) {
76
- event.preventDefault();
77
- if (this.args.isDisabled) return;
78
-
79
- this.isExpanded = true;
80
- }
81
-
82
- @action
83
- hideDropdown(event) {
84
- if (this.isExpanded) {
85
- event.preventDefault();
86
-
87
- this.isExpanded = false;
88
- }
89
- }
90
-
91
- @action
92
- onChange(option, event) {
93
- if (this.args.isDisabled) return;
94
-
95
- this.args.onChange(option.value);
96
-
97
- this.hideDropdown(event);
98
- document.getElementById(this.selectId).focus();
99
- }
100
-
101
- @action
102
- setSearchValue(event) {
103
- this.searchValue = event.target.value.trim();
104
- }
105
-
106
- @action
107
- lockTab(event) {
108
- if (event.code === 'Tab' && this.isExpanded) {
109
- event.preventDefault();
110
- if (this.args.isSearchable) document.getElementById(this.searchId).focus();
111
- }
112
- }
113
-
114
- @action
115
- focus(event) {
116
- if (!event.target) return;
117
- if (!this.isExpanded) return;
118
-
119
- if (this.args.value) {
120
- event.target.querySelector("[aria-selected='true']")?.focus();
121
- } else if (this.args.isSearchable) {
122
- event.target.querySelector(`#${this.searchId}`)?.focus();
123
- } else if (this.displayDefaultOption) {
124
- event.target.querySelector("[aria-selected='true']")?.focus();
125
- }
126
- }
127
- }