govuk_publishing_components 58.1.1 → 59.0.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.
- checksums.yaml +4 -4
- data/app/assets/images/select-with-search/cross-icon.svg +6 -0
- data/app/assets/javascripts/govuk_publishing_components/analytics-ga4/ga4-search-tracker.js +4 -0
- data/app/assets/javascripts/govuk_publishing_components/components/select-with-search.js +57 -0
- data/app/assets/stylesheets/govuk_publishing_components/_all_components.scss +2 -0
- data/app/assets/stylesheets/govuk_publishing_components/components/_action-link.scss +14 -136
- data/app/assets/stylesheets/govuk_publishing_components/components/_heading.scss +0 -5
- data/app/assets/stylesheets/govuk_publishing_components/components/_layout-header.scss +6 -48
- data/app/assets/stylesheets/govuk_publishing_components/components/_select-with-search.scss +168 -0
- data/app/assets/stylesheets/govuk_publishing_components/components/_select.scss +6 -0
- data/app/assets/stylesheets/govuk_publishing_components/components/_tag.scss +14 -0
- data/app/assets/stylesheets/govuk_publishing_components/components/helpers/_markdown-typography.scss +1 -1
- data/app/views/govuk_publishing_components/components/_action_link.html.erb +4 -37
- data/app/views/govuk_publishing_components/components/_layout_header.html.erb +0 -2
- data/app/views/govuk_publishing_components/components/_select.html.erb +22 -23
- data/app/views/govuk_publishing_components/components/_select_with_search.html.erb +14 -0
- data/app/views/govuk_publishing_components/components/_share_links.html.erb +17 -9
- data/app/views/govuk_publishing_components/components/_tag.html.erb +20 -0
- data/app/views/govuk_publishing_components/components/docs/action_link.yml +0 -62
- data/app/views/govuk_publishing_components/components/docs/layout_header.yml +13 -0
- data/app/views/govuk_publishing_components/components/docs/select.yml +11 -0
- data/app/views/govuk_publishing_components/components/docs/select_with_search.yml +196 -0
- data/app/views/govuk_publishing_components/components/docs/share_links.yml +10 -0
- data/app/views/govuk_publishing_components/components/docs/signup_link.yml +0 -7
- data/app/views/govuk_publishing_components/components/docs/single_page_notification_button.yml +1 -7
- data/app/views/govuk_publishing_components/components/docs/subscription_links.yml +1 -7
- data/app/views/govuk_publishing_components/components/docs/tag.yml +57 -0
- data/app/views/govuk_publishing_components/components/layout_header/_header_logo.html.erb +16 -5
- data/lib/govuk_publishing_components/presenters/heading_helper.rb +1 -2
- data/lib/govuk_publishing_components/presenters/select_helper.rb +8 -5
- data/lib/govuk_publishing_components/presenters/select_with_search_helper.rb +92 -0
- data/lib/govuk_publishing_components/presenters/single_page_notification_button_helper.rb +1 -1
- data/lib/govuk_publishing_components/version.rb +1 -1
- data/lib/govuk_publishing_components.rb +1 -0
- data/node_modules/choices.js/LICENSE +21 -0
- data/node_modules/choices.js/README.md +1360 -0
- data/node_modules/choices.js/package.json +173 -0
- data/node_modules/choices.js/public/assets/scripts/choices.js +5230 -0
- data/node_modules/choices.js/public/assets/scripts/choices.min.js +2 -0
- data/node_modules/choices.js/public/assets/scripts/choices.mjs +5222 -0
- data/node_modules/choices.js/public/assets/scripts/choices.search-basic.js +4748 -0
- data/node_modules/choices.js/public/assets/scripts/choices.search-basic.min.js +2 -0
- data/node_modules/choices.js/public/assets/scripts/choices.search-basic.mjs +4740 -0
- data/node_modules/choices.js/public/assets/scripts/choices.search-kmp.js +3631 -0
- data/node_modules/choices.js/public/assets/scripts/choices.search-kmp.min.js +2 -0
- data/node_modules/choices.js/public/assets/scripts/choices.search-kmp.mjs +3623 -0
- data/node_modules/choices.js/public/assets/scripts/choices.search-prefix.js +3590 -0
- data/node_modules/choices.js/public/assets/scripts/choices.search-prefix.min.js +2 -0
- data/node_modules/choices.js/public/assets/scripts/choices.search-prefix.mjs +3582 -0
- data/node_modules/choices.js/public/assets/styles/base.css +180 -0
- data/node_modules/choices.js/public/assets/styles/base.css.map +1 -0
- data/node_modules/choices.js/public/assets/styles/base.min.css +1 -0
- data/node_modules/choices.js/public/assets/styles/choices.css +338 -0
- data/node_modules/choices.js/public/assets/styles/choices.css.map +1 -0
- data/node_modules/choices.js/public/assets/styles/choices.min.css +1 -0
- data/node_modules/choices.js/public/types/src/index.d.ts +6 -0
- data/node_modules/choices.js/public/types/src/scripts/actions/choices.d.ts +30 -0
- data/node_modules/choices.js/public/types/src/scripts/actions/groups.d.ts +8 -0
- data/node_modules/choices.js/public/types/src/scripts/actions/items.d.ts +17 -0
- data/node_modules/choices.js/public/types/src/scripts/choices.d.ts +210 -0
- data/node_modules/choices.js/public/types/src/scripts/components/container.d.ts +36 -0
- data/node_modules/choices.js/public/types/src/scripts/components/dropdown.d.ts +21 -0
- data/node_modules/choices.js/public/types/src/scripts/components/index.d.ts +7 -0
- data/node_modules/choices.js/public/types/src/scripts/components/input.d.ts +37 -0
- data/node_modules/choices.js/public/types/src/scripts/components/list.d.ts +14 -0
- data/node_modules/choices.js/public/types/src/scripts/components/wrapped-element.d.ts +21 -0
- data/node_modules/choices.js/public/types/src/scripts/components/wrapped-input.d.ts +3 -0
- data/node_modules/choices.js/public/types/src/scripts/components/wrapped-select.d.ts +20 -0
- data/node_modules/choices.js/public/types/src/scripts/constants.d.ts +1 -0
- data/node_modules/choices.js/public/types/src/scripts/defaults.d.ts +4 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/action-type.d.ts +13 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/build-flags.d.ts +11 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/choice-full.d.ts +23 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/class-names.d.ts +61 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/event-choice.d.ts +7 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/event-type.d.ts +14 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/group-full.d.ts +10 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/index.d.ts +14 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/input-choice.d.ts +15 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/input-group.d.ts +10 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/item.d.ts +17 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/keycode-map.d.ts +13 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/options.d.ts +566 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/passed-element-type.d.ts +7 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/passed-element.d.ts +95 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/position-options-type.d.ts +1 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/search.d.ts +11 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/state.d.ts +10 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/store.d.ts +64 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/string-pre-escaped.d.ts +3 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/string-untrusted.d.ts +4 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/templates.d.ts +29 -0
- data/node_modules/choices.js/public/types/src/scripts/interfaces/types.d.ts +18 -0
- data/node_modules/choices.js/public/types/src/scripts/lib/choice-input.d.ts +9 -0
- data/node_modules/choices.js/public/types/src/scripts/lib/html-guard-statements.d.ts +4 -0
- data/node_modules/choices.js/public/types/src/scripts/lib/utils.d.ts +31 -0
- data/node_modules/choices.js/public/types/src/scripts/reducers/choices.d.ts +8 -0
- data/node_modules/choices.js/public/types/src/scripts/reducers/groups.d.ts +8 -0
- data/node_modules/choices.js/public/types/src/scripts/reducers/items.d.ts +9 -0
- data/node_modules/choices.js/public/types/src/scripts/search/fuse.d.ts +14 -0
- data/node_modules/choices.js/public/types/src/scripts/search/index.d.ts +3 -0
- data/node_modules/choices.js/public/types/src/scripts/search/kmp.d.ts +11 -0
- data/node_modules/choices.js/public/types/src/scripts/search/prefix-filter.d.ts +11 -0
- data/node_modules/choices.js/public/types/src/scripts/store/store.d.ts +59 -0
- data/node_modules/choices.js/public/types/src/scripts/templates.d.ts +8 -0
- data/node_modules/choices.js/src/entry.js +3 -0
- data/node_modules/choices.js/src/icons/cross-inverse.svg +1 -0
- data/node_modules/choices.js/src/icons/cross.svg +1 -0
- data/node_modules/choices.js/src/index.ts +8 -0
- data/node_modules/choices.js/src/scripts/actions/choices.ts +59 -0
- data/node_modules/choices.js/src/scripts/actions/groups.ts +14 -0
- data/node_modules/choices.js/src/scripts/actions/items.ts +34 -0
- data/node_modules/choices.js/src/scripts/choices.ts +2364 -0
- data/node_modules/choices.js/src/scripts/components/container.ts +157 -0
- data/node_modules/choices.js/src/scripts/components/dropdown.ts +50 -0
- data/node_modules/choices.js/src/scripts/components/index.ts +8 -0
- data/node_modules/choices.js/src/scripts/components/input.ts +146 -0
- data/node_modules/choices.js/src/scripts/components/list.ts +89 -0
- data/node_modules/choices.js/src/scripts/components/wrapped-element.ts +89 -0
- data/node_modules/choices.js/src/scripts/components/wrapped-input.ts +3 -0
- data/node_modules/choices.js/src/scripts/components/wrapped-select.ts +115 -0
- data/node_modules/choices.js/src/scripts/constants.ts +1 -0
- data/node_modules/choices.js/src/scripts/defaults.ts +93 -0
- data/node_modules/choices.js/src/scripts/interfaces/action-type.ts +15 -0
- data/node_modules/choices.js/src/scripts/interfaces/build-flags.ts +17 -0
- data/node_modules/choices.js/src/scripts/interfaces/choice-full.ts +30 -0
- data/node_modules/choices.js/src/scripts/interfaces/class-names.ts +61 -0
- data/node_modules/choices.js/src/scripts/interfaces/event-choice.ts +9 -0
- data/node_modules/choices.js/src/scripts/interfaces/event-type.ts +16 -0
- data/node_modules/choices.js/src/scripts/interfaces/group-full.ts +12 -0
- data/node_modules/choices.js/src/scripts/interfaces/index.ts +14 -0
- data/node_modules/choices.js/src/scripts/interfaces/input-choice.ts +17 -0
- data/node_modules/choices.js/src/scripts/interfaces/input-group.ts +11 -0
- data/node_modules/choices.js/src/scripts/interfaces/item.ts +17 -0
- data/node_modules/choices.js/src/scripts/interfaces/keycode-map.ts +13 -0
- data/node_modules/choices.js/src/scripts/interfaces/options.ts +619 -0
- data/node_modules/choices.js/src/scripts/interfaces/passed-element-type.ts +9 -0
- data/node_modules/choices.js/src/scripts/interfaces/passed-element.ts +96 -0
- data/node_modules/choices.js/src/scripts/interfaces/position-options-type.ts +1 -0
- data/node_modules/choices.js/src/scripts/interfaces/search.ts +12 -0
- data/node_modules/choices.js/src/scripts/interfaces/state.ts +12 -0
- data/node_modules/choices.js/src/scripts/interfaces/store.ts +84 -0
- data/node_modules/choices.js/src/scripts/interfaces/string-pre-escaped.ts +3 -0
- data/node_modules/choices.js/src/scripts/interfaces/string-untrusted.ts +5 -0
- data/node_modules/choices.js/src/scripts/interfaces/templates.ts +66 -0
- data/node_modules/choices.js/src/scripts/interfaces/types.ts +21 -0
- data/node_modules/choices.js/src/scripts/lib/choice-input.ts +88 -0
- data/node_modules/choices.js/src/scripts/lib/html-guard-statements.ts +7 -0
- data/node_modules/choices.js/src/scripts/lib/utils.ts +230 -0
- data/node_modules/choices.js/src/scripts/reducers/choices.ts +86 -0
- data/node_modules/choices.js/src/scripts/reducers/groups.ts +32 -0
- data/node_modules/choices.js/src/scripts/reducers/items.ts +86 -0
- data/node_modules/choices.js/src/scripts/search/fuse.ts +59 -0
- data/node_modules/choices.js/src/scripts/search/index.ts +17 -0
- data/node_modules/choices.js/src/scripts/search/kmp.ts +87 -0
- data/node_modules/choices.js/src/scripts/search/prefix-filter.ts +42 -0
- data/node_modules/choices.js/src/scripts/store/store.ts +184 -0
- data/node_modules/choices.js/src/scripts/templates.ts +409 -0
- data/node_modules/choices.js/src/styles/base.scss +189 -0
- data/node_modules/choices.js/src/styles/choices.scss +414 -0
- data/node_modules/choices.js/src/tsconfig.json +22 -0
- metadata +137 -4
- data/app/assets/images/govuk_publishing_components/action-link-arrow--dark.svg +0 -5
- data/app/assets/images/govuk_publishing_components/action-link-arrow--simple-light.svg +0 -4
- data/app/assets/images/govuk_publishing_components/action-link-arrow.svg +0 -5
@@ -0,0 +1,157 @@
|
|
1
|
+
import { addClassesToElement, removeClassesFromElement } from '../lib/utils';
|
2
|
+
import { ClassNames } from '../interfaces/class-names';
|
3
|
+
import { PositionOptionsType } from '../interfaces/position-options-type';
|
4
|
+
import { PassedElementType, PassedElementTypes } from '../interfaces/passed-element-type';
|
5
|
+
|
6
|
+
export default class Container {
|
7
|
+
element: HTMLElement;
|
8
|
+
|
9
|
+
type: PassedElementType;
|
10
|
+
|
11
|
+
classNames: ClassNames;
|
12
|
+
|
13
|
+
position: PositionOptionsType;
|
14
|
+
|
15
|
+
isOpen: boolean;
|
16
|
+
|
17
|
+
isFlipped: boolean;
|
18
|
+
|
19
|
+
isDisabled: boolean;
|
20
|
+
|
21
|
+
isLoading: boolean;
|
22
|
+
|
23
|
+
constructor({
|
24
|
+
element,
|
25
|
+
type,
|
26
|
+
classNames,
|
27
|
+
position,
|
28
|
+
}: {
|
29
|
+
element: HTMLElement;
|
30
|
+
type: PassedElementType;
|
31
|
+
classNames: ClassNames;
|
32
|
+
position: PositionOptionsType;
|
33
|
+
}) {
|
34
|
+
this.element = element;
|
35
|
+
this.classNames = classNames;
|
36
|
+
this.type = type;
|
37
|
+
this.position = position;
|
38
|
+
this.isOpen = false;
|
39
|
+
this.isFlipped = false;
|
40
|
+
this.isDisabled = false;
|
41
|
+
this.isLoading = false;
|
42
|
+
}
|
43
|
+
|
44
|
+
/**
|
45
|
+
* Determine whether container should be flipped based on passed
|
46
|
+
* dropdown position
|
47
|
+
*/
|
48
|
+
shouldFlip(dropdownPos: number, dropdownHeight: number): boolean {
|
49
|
+
// If flip is enabled and the dropdown bottom position is
|
50
|
+
// greater than the window height flip the dropdown.
|
51
|
+
let shouldFlip = false;
|
52
|
+
if (this.position === 'auto') {
|
53
|
+
shouldFlip =
|
54
|
+
this.element.getBoundingClientRect().top - dropdownHeight >= 0 &&
|
55
|
+
!window.matchMedia(`(min-height: ${dropdownPos + 1}px)`).matches;
|
56
|
+
} else if (this.position === 'top') {
|
57
|
+
shouldFlip = true;
|
58
|
+
}
|
59
|
+
|
60
|
+
return shouldFlip;
|
61
|
+
}
|
62
|
+
|
63
|
+
setActiveDescendant(activeDescendantID: string): void {
|
64
|
+
this.element.setAttribute('aria-activedescendant', activeDescendantID);
|
65
|
+
}
|
66
|
+
|
67
|
+
removeActiveDescendant(): void {
|
68
|
+
this.element.removeAttribute('aria-activedescendant');
|
69
|
+
}
|
70
|
+
|
71
|
+
open(dropdownPos: number, dropdownHeight: number): void {
|
72
|
+
addClassesToElement(this.element, this.classNames.openState);
|
73
|
+
this.element.setAttribute('aria-expanded', 'true');
|
74
|
+
this.isOpen = true;
|
75
|
+
|
76
|
+
if (this.shouldFlip(dropdownPos, dropdownHeight)) {
|
77
|
+
addClassesToElement(this.element, this.classNames.flippedState);
|
78
|
+
this.isFlipped = true;
|
79
|
+
}
|
80
|
+
}
|
81
|
+
|
82
|
+
close(): void {
|
83
|
+
removeClassesFromElement(this.element, this.classNames.openState);
|
84
|
+
this.element.setAttribute('aria-expanded', 'false');
|
85
|
+
this.removeActiveDescendant();
|
86
|
+
this.isOpen = false;
|
87
|
+
|
88
|
+
// A dropdown flips if it does not have space within the page
|
89
|
+
if (this.isFlipped) {
|
90
|
+
removeClassesFromElement(this.element, this.classNames.flippedState);
|
91
|
+
this.isFlipped = false;
|
92
|
+
}
|
93
|
+
}
|
94
|
+
|
95
|
+
addFocusState(): void {
|
96
|
+
addClassesToElement(this.element, this.classNames.focusState);
|
97
|
+
}
|
98
|
+
|
99
|
+
removeFocusState(): void {
|
100
|
+
removeClassesFromElement(this.element, this.classNames.focusState);
|
101
|
+
}
|
102
|
+
|
103
|
+
enable(): void {
|
104
|
+
removeClassesFromElement(this.element, this.classNames.disabledState);
|
105
|
+
this.element.removeAttribute('aria-disabled');
|
106
|
+
if (this.type === PassedElementTypes.SelectOne) {
|
107
|
+
this.element.setAttribute('tabindex', '0');
|
108
|
+
}
|
109
|
+
this.isDisabled = false;
|
110
|
+
}
|
111
|
+
|
112
|
+
disable(): void {
|
113
|
+
addClassesToElement(this.element, this.classNames.disabledState);
|
114
|
+
this.element.setAttribute('aria-disabled', 'true');
|
115
|
+
if (this.type === PassedElementTypes.SelectOne) {
|
116
|
+
this.element.setAttribute('tabindex', '-1');
|
117
|
+
}
|
118
|
+
this.isDisabled = true;
|
119
|
+
}
|
120
|
+
|
121
|
+
wrap(element: HTMLElement): void {
|
122
|
+
const el = this.element;
|
123
|
+
const { parentNode } = element;
|
124
|
+
if (parentNode) {
|
125
|
+
if (element.nextSibling) {
|
126
|
+
parentNode.insertBefore(el, element.nextSibling);
|
127
|
+
} else {
|
128
|
+
parentNode.appendChild(el);
|
129
|
+
}
|
130
|
+
}
|
131
|
+
|
132
|
+
el.appendChild(element);
|
133
|
+
}
|
134
|
+
|
135
|
+
unwrap(element: HTMLElement): void {
|
136
|
+
const el = this.element;
|
137
|
+
const { parentNode } = el;
|
138
|
+
if (parentNode) {
|
139
|
+
// Move passed element outside this element
|
140
|
+
parentNode.insertBefore(element, el);
|
141
|
+
// Remove this element
|
142
|
+
parentNode.removeChild(el);
|
143
|
+
}
|
144
|
+
}
|
145
|
+
|
146
|
+
addLoadingState(): void {
|
147
|
+
addClassesToElement(this.element, this.classNames.loadingState);
|
148
|
+
this.element.setAttribute('aria-busy', 'true');
|
149
|
+
this.isLoading = true;
|
150
|
+
}
|
151
|
+
|
152
|
+
removeLoadingState(): void {
|
153
|
+
removeClassesFromElement(this.element, this.classNames.loadingState);
|
154
|
+
this.element.removeAttribute('aria-busy');
|
155
|
+
this.isLoading = false;
|
156
|
+
}
|
157
|
+
}
|
@@ -0,0 +1,50 @@
|
|
1
|
+
import { ClassNames } from '../interfaces/class-names';
|
2
|
+
import { PassedElementType } from '../interfaces/passed-element-type';
|
3
|
+
import { addClassesToElement, removeClassesFromElement } from '../lib/utils';
|
4
|
+
|
5
|
+
export default class Dropdown {
|
6
|
+
element: HTMLElement;
|
7
|
+
|
8
|
+
type: PassedElementType;
|
9
|
+
|
10
|
+
classNames: ClassNames;
|
11
|
+
|
12
|
+
isActive: boolean;
|
13
|
+
|
14
|
+
constructor({
|
15
|
+
element,
|
16
|
+
type,
|
17
|
+
classNames,
|
18
|
+
}: {
|
19
|
+
element: HTMLElement;
|
20
|
+
type: PassedElementType;
|
21
|
+
classNames: ClassNames;
|
22
|
+
}) {
|
23
|
+
this.element = element;
|
24
|
+
this.classNames = classNames;
|
25
|
+
this.type = type;
|
26
|
+
this.isActive = false;
|
27
|
+
}
|
28
|
+
|
29
|
+
/**
|
30
|
+
* Show dropdown to user by adding active state class
|
31
|
+
*/
|
32
|
+
show(): this {
|
33
|
+
addClassesToElement(this.element, this.classNames.activeState);
|
34
|
+
this.element.setAttribute('aria-expanded', 'true');
|
35
|
+
this.isActive = true;
|
36
|
+
|
37
|
+
return this;
|
38
|
+
}
|
39
|
+
|
40
|
+
/**
|
41
|
+
* Hide dropdown from user
|
42
|
+
*/
|
43
|
+
hide(): this {
|
44
|
+
removeClassesFromElement(this.element, this.classNames.activeState);
|
45
|
+
this.element.setAttribute('aria-expanded', 'false');
|
46
|
+
this.isActive = false;
|
47
|
+
|
48
|
+
return this;
|
49
|
+
}
|
50
|
+
}
|
@@ -0,0 +1,8 @@
|
|
1
|
+
import Dropdown from './dropdown';
|
2
|
+
import Container from './container';
|
3
|
+
import Input from './input';
|
4
|
+
import List from './list';
|
5
|
+
import WrappedInput from './wrapped-input';
|
6
|
+
import WrappedSelect from './wrapped-select';
|
7
|
+
|
8
|
+
export { Dropdown, Container, Input, List, WrappedInput, WrappedSelect };
|
@@ -0,0 +1,146 @@
|
|
1
|
+
import { ClassNames } from '../interfaces/class-names';
|
2
|
+
import { PassedElementType, PassedElementTypes } from '../interfaces/passed-element-type';
|
3
|
+
|
4
|
+
export default class Input {
|
5
|
+
element: HTMLInputElement;
|
6
|
+
|
7
|
+
type: PassedElementType;
|
8
|
+
|
9
|
+
classNames: ClassNames;
|
10
|
+
|
11
|
+
preventPaste: boolean;
|
12
|
+
|
13
|
+
isFocussed: boolean;
|
14
|
+
|
15
|
+
isDisabled: boolean;
|
16
|
+
|
17
|
+
constructor({
|
18
|
+
element,
|
19
|
+
type,
|
20
|
+
classNames,
|
21
|
+
preventPaste,
|
22
|
+
}: {
|
23
|
+
element: HTMLInputElement;
|
24
|
+
type: PassedElementType;
|
25
|
+
classNames: ClassNames;
|
26
|
+
preventPaste: boolean;
|
27
|
+
}) {
|
28
|
+
this.element = element;
|
29
|
+
this.type = type;
|
30
|
+
this.classNames = classNames;
|
31
|
+
this.preventPaste = preventPaste;
|
32
|
+
|
33
|
+
this.isFocussed = this.element.isEqualNode(document.activeElement);
|
34
|
+
this.isDisabled = element.disabled;
|
35
|
+
this._onPaste = this._onPaste.bind(this);
|
36
|
+
this._onInput = this._onInput.bind(this);
|
37
|
+
this._onFocus = this._onFocus.bind(this);
|
38
|
+
this._onBlur = this._onBlur.bind(this);
|
39
|
+
}
|
40
|
+
|
41
|
+
set placeholder(placeholder: string) {
|
42
|
+
this.element.placeholder = placeholder;
|
43
|
+
}
|
44
|
+
|
45
|
+
get value(): string {
|
46
|
+
return this.element.value;
|
47
|
+
}
|
48
|
+
|
49
|
+
set value(value: string) {
|
50
|
+
this.element.value = value;
|
51
|
+
}
|
52
|
+
|
53
|
+
addEventListeners(): void {
|
54
|
+
const el = this.element;
|
55
|
+
el.addEventListener('paste', this._onPaste);
|
56
|
+
el.addEventListener('input', this._onInput, {
|
57
|
+
passive: true,
|
58
|
+
});
|
59
|
+
el.addEventListener('focus', this._onFocus, {
|
60
|
+
passive: true,
|
61
|
+
});
|
62
|
+
el.addEventListener('blur', this._onBlur, {
|
63
|
+
passive: true,
|
64
|
+
});
|
65
|
+
}
|
66
|
+
|
67
|
+
removeEventListeners(): void {
|
68
|
+
const el = this.element;
|
69
|
+
el.removeEventListener('input', this._onInput);
|
70
|
+
el.removeEventListener('paste', this._onPaste);
|
71
|
+
el.removeEventListener('focus', this._onFocus);
|
72
|
+
el.removeEventListener('blur', this._onBlur);
|
73
|
+
}
|
74
|
+
|
75
|
+
enable(): void {
|
76
|
+
const el = this.element;
|
77
|
+
el.removeAttribute('disabled');
|
78
|
+
this.isDisabled = false;
|
79
|
+
}
|
80
|
+
|
81
|
+
disable(): void {
|
82
|
+
const el = this.element;
|
83
|
+
el.setAttribute('disabled', '');
|
84
|
+
this.isDisabled = true;
|
85
|
+
}
|
86
|
+
|
87
|
+
focus(): void {
|
88
|
+
if (!this.isFocussed) {
|
89
|
+
this.element.focus();
|
90
|
+
}
|
91
|
+
}
|
92
|
+
|
93
|
+
blur(): void {
|
94
|
+
if (this.isFocussed) {
|
95
|
+
this.element.blur();
|
96
|
+
}
|
97
|
+
}
|
98
|
+
|
99
|
+
clear(setWidth = true): this {
|
100
|
+
this.element.value = '';
|
101
|
+
if (setWidth) {
|
102
|
+
this.setWidth();
|
103
|
+
}
|
104
|
+
|
105
|
+
return this;
|
106
|
+
}
|
107
|
+
|
108
|
+
/**
|
109
|
+
* Set the correct input width based on placeholder
|
110
|
+
* value or input value
|
111
|
+
*/
|
112
|
+
setWidth(): void {
|
113
|
+
// Resize input to contents or placeholder
|
114
|
+
const { element } = this;
|
115
|
+
element.style.minWidth = `${element.placeholder.length + 1}ch`;
|
116
|
+
element.style.width = `${element.value.length + 1}ch`;
|
117
|
+
}
|
118
|
+
|
119
|
+
setActiveDescendant(activeDescendantID: string): void {
|
120
|
+
this.element.setAttribute('aria-activedescendant', activeDescendantID);
|
121
|
+
}
|
122
|
+
|
123
|
+
removeActiveDescendant(): void {
|
124
|
+
this.element.removeAttribute('aria-activedescendant');
|
125
|
+
}
|
126
|
+
|
127
|
+
_onInput(): void {
|
128
|
+
if (this.type !== PassedElementTypes.SelectOne) {
|
129
|
+
this.setWidth();
|
130
|
+
}
|
131
|
+
}
|
132
|
+
|
133
|
+
_onPaste(event: ClipboardEvent): void {
|
134
|
+
if (this.preventPaste) {
|
135
|
+
event.preventDefault();
|
136
|
+
}
|
137
|
+
}
|
138
|
+
|
139
|
+
_onFocus(): void {
|
140
|
+
this.isFocussed = true;
|
141
|
+
}
|
142
|
+
|
143
|
+
_onBlur(): void {
|
144
|
+
this.isFocussed = false;
|
145
|
+
}
|
146
|
+
}
|
@@ -0,0 +1,89 @@
|
|
1
|
+
import { SCROLLING_SPEED } from '../constants';
|
2
|
+
|
3
|
+
export default class List {
|
4
|
+
element: HTMLElement;
|
5
|
+
|
6
|
+
scrollPos: number;
|
7
|
+
|
8
|
+
height: number;
|
9
|
+
|
10
|
+
constructor({ element }: { element: HTMLElement }) {
|
11
|
+
this.element = element;
|
12
|
+
this.scrollPos = this.element.scrollTop;
|
13
|
+
this.height = this.element.offsetHeight;
|
14
|
+
}
|
15
|
+
|
16
|
+
prepend(node: Element | DocumentFragment): void {
|
17
|
+
const child = this.element.firstElementChild;
|
18
|
+
if (child) {
|
19
|
+
this.element.insertBefore(node, child);
|
20
|
+
} else {
|
21
|
+
this.element.append(node);
|
22
|
+
}
|
23
|
+
}
|
24
|
+
|
25
|
+
scrollToTop(): void {
|
26
|
+
this.element.scrollTop = 0;
|
27
|
+
}
|
28
|
+
|
29
|
+
scrollToChildElement(element: HTMLElement, direction: 1 | -1): void {
|
30
|
+
if (!element) {
|
31
|
+
return;
|
32
|
+
}
|
33
|
+
|
34
|
+
const listHeight = this.element.offsetHeight;
|
35
|
+
// Scroll position of dropdown
|
36
|
+
const listScrollPosition = this.element.scrollTop + listHeight;
|
37
|
+
|
38
|
+
const elementHeight = element.offsetHeight;
|
39
|
+
// Distance from bottom of element to top of parent
|
40
|
+
const elementPos = element.offsetTop + elementHeight;
|
41
|
+
|
42
|
+
// Difference between the element and scroll position
|
43
|
+
const destination = direction > 0 ? this.element.scrollTop + elementPos - listScrollPosition : element.offsetTop;
|
44
|
+
|
45
|
+
requestAnimationFrame(() => {
|
46
|
+
this._animateScroll(destination, direction);
|
47
|
+
});
|
48
|
+
}
|
49
|
+
|
50
|
+
_scrollDown(scrollPos: number, strength: number, destination: number): void {
|
51
|
+
const easing = (destination - scrollPos) / strength;
|
52
|
+
const distance = easing > 1 ? easing : 1;
|
53
|
+
|
54
|
+
this.element.scrollTop = scrollPos + distance;
|
55
|
+
}
|
56
|
+
|
57
|
+
_scrollUp(scrollPos: number, strength: number, destination: number): void {
|
58
|
+
const easing = (scrollPos - destination) / strength;
|
59
|
+
const distance = easing > 1 ? easing : 1;
|
60
|
+
|
61
|
+
this.element.scrollTop = scrollPos - distance;
|
62
|
+
}
|
63
|
+
|
64
|
+
_animateScroll(destination: number, direction: number): void {
|
65
|
+
const strength = SCROLLING_SPEED;
|
66
|
+
const choiceListScrollTop = this.element.scrollTop;
|
67
|
+
let continueAnimation = false;
|
68
|
+
|
69
|
+
if (direction > 0) {
|
70
|
+
this._scrollDown(choiceListScrollTop, strength, destination);
|
71
|
+
|
72
|
+
if (choiceListScrollTop < destination) {
|
73
|
+
continueAnimation = true;
|
74
|
+
}
|
75
|
+
} else {
|
76
|
+
this._scrollUp(choiceListScrollTop, strength, destination);
|
77
|
+
|
78
|
+
if (choiceListScrollTop > destination) {
|
79
|
+
continueAnimation = true;
|
80
|
+
}
|
81
|
+
}
|
82
|
+
|
83
|
+
if (continueAnimation) {
|
84
|
+
requestAnimationFrame(() => {
|
85
|
+
this._animateScroll(destination, direction);
|
86
|
+
});
|
87
|
+
}
|
88
|
+
}
|
89
|
+
}
|
@@ -0,0 +1,89 @@
|
|
1
|
+
import { ClassNames } from '../interfaces/class-names';
|
2
|
+
import { EventTypes } from '../interfaces/event-type';
|
3
|
+
import { addClassesToElement, dispatchEvent, removeClassesFromElement } from '../lib/utils';
|
4
|
+
import { EventMap } from '../interfaces';
|
5
|
+
|
6
|
+
export default class WrappedElement<T extends HTMLInputElement | HTMLSelectElement> {
|
7
|
+
element: T;
|
8
|
+
|
9
|
+
classNames: ClassNames;
|
10
|
+
|
11
|
+
isDisabled: boolean;
|
12
|
+
|
13
|
+
constructor({ element, classNames }) {
|
14
|
+
this.element = element;
|
15
|
+
this.classNames = classNames;
|
16
|
+
this.isDisabled = false;
|
17
|
+
}
|
18
|
+
|
19
|
+
get isActive(): boolean {
|
20
|
+
return this.element.dataset.choice === 'active';
|
21
|
+
}
|
22
|
+
|
23
|
+
get dir(): string {
|
24
|
+
return this.element.dir;
|
25
|
+
}
|
26
|
+
|
27
|
+
get value(): string {
|
28
|
+
return this.element.value;
|
29
|
+
}
|
30
|
+
|
31
|
+
set value(value: string) {
|
32
|
+
this.element.setAttribute('value', value);
|
33
|
+
this.element.value = value;
|
34
|
+
}
|
35
|
+
|
36
|
+
conceal(): void {
|
37
|
+
const el = this.element;
|
38
|
+
// Hide passed input
|
39
|
+
addClassesToElement(el, this.classNames.input);
|
40
|
+
el.hidden = true;
|
41
|
+
|
42
|
+
// Remove element from tab index
|
43
|
+
el.tabIndex = -1;
|
44
|
+
|
45
|
+
// Backup original styles if any
|
46
|
+
const origStyle = el.getAttribute('style');
|
47
|
+
|
48
|
+
if (origStyle) {
|
49
|
+
el.setAttribute('data-choice-orig-style', origStyle);
|
50
|
+
}
|
51
|
+
|
52
|
+
el.setAttribute('data-choice', 'active');
|
53
|
+
}
|
54
|
+
|
55
|
+
reveal(): void {
|
56
|
+
const el = this.element;
|
57
|
+
// Reinstate passed element
|
58
|
+
removeClassesFromElement(el, this.classNames.input);
|
59
|
+
el.hidden = false;
|
60
|
+
el.removeAttribute('tabindex');
|
61
|
+
|
62
|
+
// Recover original styles if any
|
63
|
+
const origStyle = el.getAttribute('data-choice-orig-style');
|
64
|
+
|
65
|
+
if (origStyle) {
|
66
|
+
el.removeAttribute('data-choice-orig-style');
|
67
|
+
el.setAttribute('style', origStyle);
|
68
|
+
} else {
|
69
|
+
el.removeAttribute('style');
|
70
|
+
}
|
71
|
+
el.removeAttribute('data-choice');
|
72
|
+
}
|
73
|
+
|
74
|
+
enable(): void {
|
75
|
+
this.element.removeAttribute('disabled');
|
76
|
+
this.element.disabled = false;
|
77
|
+
this.isDisabled = false;
|
78
|
+
}
|
79
|
+
|
80
|
+
disable(): void {
|
81
|
+
this.element.setAttribute('disabled', '');
|
82
|
+
this.element.disabled = true;
|
83
|
+
this.isDisabled = true;
|
84
|
+
}
|
85
|
+
|
86
|
+
triggerEvent<K extends EventTypes>(eventType: EventTypes, data?: EventMap[K]['detail']): void {
|
87
|
+
dispatchEvent(this.element, eventType, data || {});
|
88
|
+
}
|
89
|
+
}
|
@@ -0,0 +1,115 @@
|
|
1
|
+
import { parseCustomProperties } from '../lib/utils';
|
2
|
+
import { ClassNames } from '../interfaces/class-names';
|
3
|
+
import WrappedElement from './wrapped-element';
|
4
|
+
import { GroupFull } from '../interfaces/group-full';
|
5
|
+
import { ChoiceFull } from '../interfaces/choice-full';
|
6
|
+
import { stringToHtmlClass } from '../lib/choice-input';
|
7
|
+
import { isHtmlOptgroup, isHtmlOption } from '../lib/html-guard-statements';
|
8
|
+
|
9
|
+
export default class WrappedSelect extends WrappedElement<HTMLSelectElement> {
|
10
|
+
classNames: ClassNames;
|
11
|
+
|
12
|
+
template: (data: object) => HTMLOptionElement;
|
13
|
+
|
14
|
+
extractPlaceholder: boolean;
|
15
|
+
|
16
|
+
constructor({
|
17
|
+
element,
|
18
|
+
classNames,
|
19
|
+
template,
|
20
|
+
extractPlaceholder,
|
21
|
+
}: {
|
22
|
+
element: HTMLSelectElement;
|
23
|
+
classNames: ClassNames;
|
24
|
+
template: (data: object) => HTMLOptionElement;
|
25
|
+
extractPlaceholder: boolean;
|
26
|
+
}) {
|
27
|
+
super({ element, classNames });
|
28
|
+
this.template = template;
|
29
|
+
this.extractPlaceholder = extractPlaceholder;
|
30
|
+
}
|
31
|
+
|
32
|
+
get placeholderOption(): HTMLOptionElement | null {
|
33
|
+
return (
|
34
|
+
this.element.querySelector('option[value=""]') ||
|
35
|
+
// Backward compatibility layer for the non-standard placeholder attribute supported in older versions.
|
36
|
+
this.element.querySelector('option[placeholder]')
|
37
|
+
);
|
38
|
+
}
|
39
|
+
|
40
|
+
addOptions(choices: ChoiceFull[]): void {
|
41
|
+
const fragment = document.createDocumentFragment();
|
42
|
+
choices.forEach((obj) => {
|
43
|
+
const choice = obj;
|
44
|
+
if (choice.element) {
|
45
|
+
return;
|
46
|
+
}
|
47
|
+
|
48
|
+
const option = this.template(choice);
|
49
|
+
fragment.appendChild(option);
|
50
|
+
choice.element = option;
|
51
|
+
});
|
52
|
+
this.element.appendChild(fragment);
|
53
|
+
}
|
54
|
+
|
55
|
+
optionsAsChoices(): (ChoiceFull | GroupFull)[] {
|
56
|
+
const choices: (ChoiceFull | GroupFull)[] = [];
|
57
|
+
|
58
|
+
this.element.querySelectorAll<HTMLElement>(':scope > option, :scope > optgroup').forEach((e) => {
|
59
|
+
if (isHtmlOption(e)) {
|
60
|
+
choices.push(this._optionToChoice(e));
|
61
|
+
} else if (isHtmlOptgroup(e)) {
|
62
|
+
choices.push(this._optgroupToChoice(e));
|
63
|
+
}
|
64
|
+
// todo: hr as empty optgroup, requires displaying empty opt-groups to be useful
|
65
|
+
});
|
66
|
+
|
67
|
+
return choices;
|
68
|
+
}
|
69
|
+
|
70
|
+
// eslint-disable-next-line class-methods-use-this
|
71
|
+
_optionToChoice(option: HTMLOptionElement): ChoiceFull {
|
72
|
+
// option.value returns the label if there is no value attribute, which can break legacy placeholder attribute support
|
73
|
+
if (!option.hasAttribute('value') && option.hasAttribute('placeholder')) {
|
74
|
+
option.setAttribute('value', '');
|
75
|
+
option.value = '';
|
76
|
+
}
|
77
|
+
|
78
|
+
return {
|
79
|
+
id: 0,
|
80
|
+
group: null,
|
81
|
+
score: 0,
|
82
|
+
rank: 0,
|
83
|
+
value: option.value,
|
84
|
+
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/option
|
85
|
+
// This attribute is text for the label indicating the meaning of the option. If the `label` attribute isn't defined, its value is that of the element text content (ie `innerText`).
|
86
|
+
label: option.label,
|
87
|
+
element: option,
|
88
|
+
active: true,
|
89
|
+
// this returns true if nothing is selected on initial load, which will break placeholder support
|
90
|
+
selected: this.extractPlaceholder ? option.selected : option.hasAttribute('selected'),
|
91
|
+
disabled: option.disabled,
|
92
|
+
highlighted: false,
|
93
|
+
placeholder: this.extractPlaceholder && (!option.value || option.hasAttribute('placeholder')),
|
94
|
+
labelClass:
|
95
|
+
typeof option.dataset.labelClass !== 'undefined' ? stringToHtmlClass(option.dataset.labelClass) : undefined,
|
96
|
+
labelDescription:
|
97
|
+
typeof option.dataset.labelDescription !== 'undefined' ? option.dataset.labelDescription : undefined,
|
98
|
+
customProperties: parseCustomProperties(option.dataset.customProperties),
|
99
|
+
};
|
100
|
+
}
|
101
|
+
|
102
|
+
_optgroupToChoice(optgroup: HTMLOptGroupElement): GroupFull {
|
103
|
+
const options = optgroup.querySelectorAll('option');
|
104
|
+
const choices = Array.from(options).map((option) => this._optionToChoice(option));
|
105
|
+
|
106
|
+
return {
|
107
|
+
id: 0,
|
108
|
+
label: optgroup.label || '',
|
109
|
+
element: optgroup,
|
110
|
+
active: !!choices.length,
|
111
|
+
disabled: optgroup.disabled,
|
112
|
+
choices,
|
113
|
+
};
|
114
|
+
}
|
115
|
+
}
|
@@ -0,0 +1 @@
|
|
1
|
+
export const SCROLLING_SPEED: number = 4 as const;
|