@utahdts/utah-design-system-header 1.2.0 → 1.3.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.
- package/dist/index.d.ts +576 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/style.css +1 -1
- package/dist/utah-design-system-header.es.js +614 -595
- package/dist/utah-design-system-header.umd.js +47 -27
- package/package.json +5 -4
- package/src/css/media-queries.css +5 -0
- package/src/index.js +11 -0
- package/src/js/enumerations/childrenMenuTypes.js +13 -0
- package/src/js/enumerations/domConstants.js +149 -0
- package/src/js/enumerations/environments.js +16 -0
- package/src/js/enumerations/events.js +14 -0
- package/src/js/enumerations/popupPlacement.js +32 -0
- package/src/js/enumerations/sizes.js +12 -0
- package/src/js/enumerations/utahIdUrls.js +10 -0
- package/src/js/lifecycle/globalEvents.js +81 -0
- package/src/js/lifecycle/hookupMobileActionItemKeyboarding.js +114 -0
- package/src/js/lifecycle/lifecycle.js +180 -0
- package/src/js/misc/checkForError.js +16 -0
- package/src/js/misc/findRecursive.js +39 -0
- package/src/js/misc/isString.js +8 -0
- package/src/js/misc/isTouchDevice.js +9 -0
- package/src/js/misc/jsDocTypes.js +235 -0
- package/src/js/misc/notNull.js +15 -0
- package/src/js/misc/popupFocusHandler.js +209 -0
- package/src/js/misc/renderDOMSingle.js +61 -0
- package/src/js/misc/showHideElement.js +15 -0
- package/src/js/misc/toHash.js +18 -0
- package/src/js/misc/uuidv4.js +8 -0
- package/src/js/misc/valueOrFunctionValue.js +13 -0
- package/src/js/renderables/_html/NewTabAccessibility.html +4 -0
- package/src/js/renderables/actionItems/ActionItems.js +29 -0
- package/src/js/renderables/actionItems/html/ActionItem.html +5 -0
- package/src/js/renderables/actionItems/html/ActionItemMenuContent.html +3 -0
- package/src/js/renderables/actionItems/html/ActionItemsWrapper.html +1 -0
- package/src/js/renderables/actionItems/html/BadgeWrapperHtml.html +4 -0
- package/src/js/renderables/actionItems/html/MobileActionItem.html +7 -0
- package/src/js/renderables/actionItems/renderActionItem.js +103 -0
- package/src/js/renderables/actionItems/renderActionItemBadge.js +50 -0
- package/src/js/renderables/actionItems/renderMobileActionItem.js +97 -0
- package/src/js/renderables/actionItems/renderMobileActionItems.js +83 -0
- package/src/js/renderables/citizenExperience/CitizenExperience.js +24 -0
- package/src/js/renderables/citizenExperience/html/CitizenExperienceWrapper.html +1 -0
- package/src/js/renderables/citizenExperience/html/CitizenExperienceWrapperMobile.html +1 -0
- package/src/js/renderables/footer/html/Footer.html +41 -0
- package/src/js/renderables/footer/renderFooter.js +111 -0
- package/src/js/renderables/headerWrapper/HeaderWrapper.js +46 -0
- package/src/js/renderables/headerWrapper/html/HeaderLogoWrapper.html +1 -0
- package/src/js/renderables/headerWrapper/html/HeaderWrapper.html +1 -0
- package/src/js/renderables/headerWrapper/html/VerticalLine.html +1 -0
- package/src/js/renderables/icons/html/AlertIcon.html +1 -0
- package/src/js/renderables/icons/html/ChevronIcon.html +1 -0
- package/src/js/renderables/icons/html/ExternalLinkIcon.html +7 -0
- package/src/js/renderables/icons/html/GearIcon.html +1 -0
- package/src/js/renderables/icons/html/QuestionIcon.html +1 -0
- package/src/js/renderables/icons/html/WaffleIcon.html +1 -0
- package/src/js/renderables/logoTitle/LogoTitle.js +63 -0
- package/src/js/renderables/logoTitle/html/LogoTitleWrapper.html +4 -0
- package/src/js/renderables/logoTitle/html/LogoTitleWrapperLink.html +4 -0
- package/src/js/renderables/mainMenu/html/MainMenuItem.html +13 -0
- package/src/js/renderables/mainMenu/html/MainMenuWrapper.html +26 -0
- package/src/js/renderables/mainMenu/renderMainMenu.js +258 -0
- package/src/js/renderables/menu/html/MenuWithTitle.html +3 -0
- package/src/js/renderables/menu/renderMenuWithTitle.js +24 -0
- package/src/js/renderables/mobile/addMobileMenuContentItem.js +37 -0
- package/src/js/renderables/mobile/hookupHamburger.js +112 -0
- package/src/js/renderables/mobile/hookupUtahIdInMobileMenu.js +79 -0
- package/src/js/renderables/mobile/html/MobileMenuContentItemWrapper.html +2 -0
- package/src/js/renderables/mobile/html/MobileMenuWrapper.html +32 -0
- package/src/js/renderables/mobile/mobileMenuInteractionHandler.js +155 -0
- package/src/js/renderables/mobile/renderMobileMenuHomeMenu.js +15 -0
- package/src/js/renderables/mobile/util/getHamburgerElements.js +22 -0
- package/src/js/renderables/mobile/util/showHideHamburgerElements.js +32 -0
- package/src/js/renderables/popup/html/Popup.html +5 -0
- package/src/js/renderables/popup/renderPopup.js +41 -0
- package/src/js/renderables/popupMenu/html/PopupMenu.html +1 -0
- package/src/js/renderables/popupMenu/html/PopupMenuItem.html +12 -0
- package/src/js/renderables/popupMenu/renderPopupMenu.js +341 -0
- package/src/js/renderables/search/html/SearchModal.html +25 -0
- package/src/js/renderables/search/showSearchModal.js +132 -0
- package/src/js/renderables/tooltip/hookupTooltip.js +82 -0
- package/src/js/renderables/tooltip/html/Tooltip.html +5 -0
- package/src/js/renderables/utahId/UtahId.js +227 -0
- package/src/js/renderables/utahId/html/UtahIdButton.html +1 -0
- package/src/js/renderables/utahId/html/UtahIdWrapper.html +1 -0
- package/src/js/renderables/utahLogo/UtahLogo.js +48 -0
- package/src/js/renderables/utahLogo/html/UtahLogoLarge.html +17 -0
- package/src/js/renderables/utahLogo/html/UtahLogoMedium.html +17 -0
- package/src/js/renderables/utahLogo/html/UtahOfficialWebsiteHoverContent.html +4 -0
- package/src/js/renderables/utahLogo/html/UtahOfficialWebsitePopupContent.html +41 -0
- package/src/js/renderables/utahLogo/renderOfficialWebsite.js +139 -0
- package/src/js/settings/defaultSettings.js +31 -0
- package/src/js/settings/getUtahHeaderSettings.js +16 -0
- package/src/js/settings/settings.js +69 -0
- package/src/js/settings/settingsKeeper.js +47 -0
- package/src/js/utahId/httpRequest.js +42 -0
- package/src/js/utahId/utahIdData.js +165 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* recursively searches through an object for fields that match a custom function like when a menu is
|
|
5
|
+
* searching if it or any of its children are selected.
|
|
6
|
+
*
|
|
7
|
+
* @template T
|
|
8
|
+
* @param {T[] | T} object the object on which to start searching
|
|
9
|
+
* @param {string[]} recursiveFields which fields on the object should be "dug" in to recursively
|
|
10
|
+
* @param {(o: T) => boolean} isMatchFunc the function to call on each child to see if it matches
|
|
11
|
+
* @returns {boolean} true if the isMatchFunc is true for the root or any of its children
|
|
12
|
+
*/
|
|
13
|
+
export default function findRecursive(object, recursiveFields, isMatchFunc) {
|
|
14
|
+
/** @type {boolean} */
|
|
15
|
+
let result = false;
|
|
16
|
+
|
|
17
|
+
if (object) {
|
|
18
|
+
if (Array.isArray(object)) {
|
|
19
|
+
// try each item in the array separately
|
|
20
|
+
result = object.some((o) => findRecursive(o, recursiveFields, isMatchFunc));
|
|
21
|
+
} else {
|
|
22
|
+
// does this object match?
|
|
23
|
+
result = isMatchFunc(object);
|
|
24
|
+
|
|
25
|
+
if (!result) {
|
|
26
|
+
// object didn't match so try its children
|
|
27
|
+
result = (
|
|
28
|
+
!!recursiveFields
|
|
29
|
+
// @ts-ignore
|
|
30
|
+
?.filter((field) => object[field])
|
|
31
|
+
// @ts-ignore
|
|
32
|
+
?.some((field) => findRecursive(object[field], recursiveFields, isMatchFunc))
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// https://www.geeksforgeeks.org/how-to-detect-touch-screen-device-using-javascript/
|
|
2
|
+
export default function isTouchDevice() {
|
|
3
|
+
return (
|
|
4
|
+
('ontouchstart' in window)
|
|
5
|
+
|| (navigator.maxTouchPoints > 0)
|
|
6
|
+
// @ts-ignore
|
|
7
|
+
|| (navigator.msMaxTouchPoints > 0)
|
|
8
|
+
);
|
|
9
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
// Types of Events: https://www.w3schools.com/jsref/obj_events.asp
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
*
|
|
7
|
+
* JsDoc types for the utah header. Types help lower the mental load of object properties as well as
|
|
8
|
+
* helps find "bugs" from invalid typings scenarios.
|
|
9
|
+
*
|
|
10
|
+
* @typedef {Element} ChildNode
|
|
11
|
+
*
|
|
12
|
+
* @typedef {(function(MouseEvent | TouchEvent | KeyboardEvent): void)} EventAction
|
|
13
|
+
*
|
|
14
|
+
* @interface Partial - typescript has "Partial" built-in while JSDoc does not know about "type utilities"
|
|
15
|
+
*
|
|
16
|
+
* @typedef {'SMALL' | 'MEDIUM' | 'LARGE'} Size
|
|
17
|
+
* Should be Synced with the enumerations/sizes object
|
|
18
|
+
*
|
|
19
|
+
* @typedef {'dialog' | 'grid' | 'listbox' | 'menu' | 'tree'} AriaHasPopupType
|
|
20
|
+
*
|
|
21
|
+
* @typedef MainMenuItem {
|
|
22
|
+
* // this started as a copy of MenuItem but can diverge to be its own thing
|
|
23
|
+
* // wordpress has the concept of a menu item that is a link AND has children, but when it goes mobile the link is no longer available
|
|
24
|
+
* // so the wordpress conversion script will take a menu that has a link and children and auto insert the link as the first child
|
|
25
|
+
* // so this could do that work automatically if it detects that scenario instead of throwing an error
|
|
26
|
+
*
|
|
27
|
+
* // should be only one of the following three action types
|
|
28
|
+
* // actionUrl: an <a> with a url for navigation
|
|
29
|
+
* // actionFunction: a <button> that when triggered calls a function
|
|
30
|
+
* // actionFunctionUrl: an <a> that when triggered calls a function (for Single Page Apps)
|
|
31
|
+
* // actionMenu: a <button> that when triggered exposes a sub menu of options
|
|
32
|
+
* @property {MenuItemUrlAction} [actionUrl] - link url
|
|
33
|
+
* @property {EventAction} [actionFunction] - onClick function
|
|
34
|
+
* @property {MenuItemFunctionUrlAction} [actionFunctionUrl] - single page apps render an <a> but call browser push; you should handle cmd click
|
|
35
|
+
* @property {MenuItem[]} [actionMenu] - children menus
|
|
36
|
+
*
|
|
37
|
+
* @property {ChildrenMenuType} [childrenMenuType] - default is "flyout"
|
|
38
|
+
* @property {string} [className] - can be used for `selected` or any other purpose
|
|
39
|
+
* @property {ChildNode} [icon] - icon to show next to this menu item
|
|
40
|
+
* @property {boolean} [isDivider] - this menu item is a divider between other menu items
|
|
41
|
+
* @property {boolean} [isSelected] - is this menu item currently a selected thing (on its page?)
|
|
42
|
+
* @property {string} title - title for the menu item
|
|
43
|
+
* }
|
|
44
|
+
*
|
|
45
|
+
* @typedef {'none' | 'a1' | 'a2' | 'a3' | 'custom' | 'unittest'} Environments
|
|
46
|
+
*
|
|
47
|
+
* @typedef {'utahHeaderLoaded' | 'utahHeaderUnloaded'} Events
|
|
48
|
+
*
|
|
49
|
+
* @typedef MainMenu {
|
|
50
|
+
* @property {MainMenuItem[]} menuItems
|
|
51
|
+
* @property {string} title
|
|
52
|
+
* }
|
|
53
|
+
*
|
|
54
|
+
* For menu items that are links to other locations
|
|
55
|
+
* @typedef MenuItemUrlAction {
|
|
56
|
+
* @property {string} url - the url to which to go when interacted with
|
|
57
|
+
* @property {boolean} [openInNewTab] - true to have the link open in a new window defaults to `false` if not provided
|
|
58
|
+
* }
|
|
59
|
+
*
|
|
60
|
+
* @typedef MenuItemFunctionUrlAction {
|
|
61
|
+
* // actually going to call this function to do what you want
|
|
62
|
+
* @property {EventAction} actionFunction - onClick custom function to call when triggered
|
|
63
|
+
* @property {boolean} [skipHandleEvent] - should handleEvent automatically be used to call your function to stop propagation and prevent default
|
|
64
|
+
*
|
|
65
|
+
* // trick the user to think it's a normal link; you must set these to the correct values to match your custom function
|
|
66
|
+
* @property {boolean} [openInNewTab] - true to have the link say it will open in a new window; defaults to `false`
|
|
67
|
+
* @property {string} url - the url to show when hovered
|
|
68
|
+
* }
|
|
69
|
+
*
|
|
70
|
+
* A menu item in the menu, can have children
|
|
71
|
+
* @typedef MenuItem {
|
|
72
|
+
* // should be only one of the following three action types
|
|
73
|
+
* // actionUrl: an <a> with a url for navigation
|
|
74
|
+
* // actionFunction: a <button> that when triggered calls a function
|
|
75
|
+
* // actionFunctionUrl: an <a> that when triggered calls a function (for Single Page Apps)
|
|
76
|
+
* // actionMenu: a <button> that when triggered exposes a sub menu of options
|
|
77
|
+
* @property {MenuItemUrlAction} [actionUrl] - link url
|
|
78
|
+
* @property {EventAction} [actionFunction] - onClick function
|
|
79
|
+
* @property {MenuItemFunctionUrlAction} [actionFunctionUrl] - single page apps render an <a> but call browser push; you should handle cmd click
|
|
80
|
+
* @property {MenuItem[]} [actionMenu] - children menus
|
|
81
|
+
*
|
|
82
|
+
* @property {string} [className] - can be used for `selected` or any other purpose
|
|
83
|
+
* @property {ChildNode} [icon] - icon to show next to this menu item
|
|
84
|
+
* @property {boolean} [isDivider] - this menu item is a divider between other menu items
|
|
85
|
+
* @property {boolean} [isSelected] - is this menu item currently a selected thing (on its page?)
|
|
86
|
+
* @property {string} title - title for the menu item
|
|
87
|
+
* }
|
|
88
|
+
*
|
|
89
|
+
* @typedef PopupMenu {
|
|
90
|
+
* @property {string} [className] - className to put on the popupMenu
|
|
91
|
+
* @property {MenuItem[]} menuItems - the menu items to show in the menu
|
|
92
|
+
* @property {string} title - the title of the menu
|
|
93
|
+
* }
|
|
94
|
+
*
|
|
95
|
+
* @typedef MediaSizes {
|
|
96
|
+
* @property {number} mobile - mobile sized render area
|
|
97
|
+
* @property {number} tabletLandscape - table landscape sized render area
|
|
98
|
+
* @property {number} tabletPortrait - table portrait sized render area
|
|
99
|
+
* }
|
|
100
|
+
*
|
|
101
|
+
* these match the popper's position options
|
|
102
|
+
* @typedef {'auto' | 'auto-start' | 'auto-end' |
|
|
103
|
+
* 'bottom' | 'bottom-start' | 'bottom-end' |
|
|
104
|
+
* 'left' | 'left-start' | 'left-end' |
|
|
105
|
+
* 'right' | 'right-start' | 'right-end' |
|
|
106
|
+
* 'top' | 'top-start' | 'top-end'
|
|
107
|
+
* } PopupPlacement
|
|
108
|
+
*
|
|
109
|
+
* @typedef PopupFocusHandlerOptions {
|
|
110
|
+
* @property {() => boolean} [isPerformPopup] should the popup pop open? Helpful for utahId that doesn't pop until user loaded
|
|
111
|
+
* @property {function(UIEvent): void} [onClick] custom onclick handler
|
|
112
|
+
* @property {PopupPlacement} [popupPlacement] which side should the popup place itself (defaults to bottom and popper will place where it can)
|
|
113
|
+
* @property {boolean} [preventOnClickHandling] turns off click handling for popup invocation
|
|
114
|
+
* @property {boolean} [shouldFocusOnHover] will perform the popup on hover as well as the focus event
|
|
115
|
+
* }
|
|
116
|
+
*
|
|
117
|
+
* @typedef RenderPopupOptions {
|
|
118
|
+
* @property {boolean} [removePopupArrow] allows removing the popup border arrow for flyouts
|
|
119
|
+
* }
|
|
120
|
+
*
|
|
121
|
+
* @typedef {('flyout' | 'inline' | 'mega-menu')} ChildrenMenuType
|
|
122
|
+
* @typedef RenderPopupMenuOptions {
|
|
123
|
+
* @property {ChildrenMenuType} childrenMenuType
|
|
124
|
+
* - flyout: children in new popup
|
|
125
|
+
* - inline: expandable children
|
|
126
|
+
* - mega-menu: always expanded children
|
|
127
|
+
* @property {boolean} [removePopupArrow] allows removing the popup border arrow for flyouts
|
|
128
|
+
* }
|
|
129
|
+
*
|
|
130
|
+
* @typedef Badge {
|
|
131
|
+
* @property {string} [className] - a class to add to the badge for custom formatting like color
|
|
132
|
+
* @property {string} label - the label for the screen reader to read describing the badge
|
|
133
|
+
* @property {number} [value] - the value to show in the badge
|
|
134
|
+
* }
|
|
135
|
+
*
|
|
136
|
+
* @typedef ActionItem {
|
|
137
|
+
* // should be only one of the following three action types
|
|
138
|
+
* @property {EventAction} [actionFunction] - func: onClick callback
|
|
139
|
+
* @property {PopupMenu} [actionPopupMenu] - Object[]: array of MenuItems
|
|
140
|
+
* @property {function (): HTMLElement | string} [actionDom] - ChildNode: content in a popup.
|
|
141
|
+
*
|
|
142
|
+
* @property {string} [className] - CSS classes for the action item
|
|
143
|
+
* @property {Badge} [badge] - the badge to show in the action item's badge icon
|
|
144
|
+
* @property {HTMLElement | string} icon - DOM or DOM string of icon to show
|
|
145
|
+
* @property {'left' | 'none' | 'right'} [mobileMenuLocation] - positioned right or left of the Utah ID button? not at all? default is none
|
|
146
|
+
* @property {boolean} showTitle - Should the title always be visible?
|
|
147
|
+
* @property {string} title - Title of the action item (required for accessibility)
|
|
148
|
+
* }
|
|
149
|
+
*
|
|
150
|
+
* // must use one and only one of the properties here
|
|
151
|
+
* @typedef DomLocationTarget {
|
|
152
|
+
* @property {string} [cssSelector] - find a target DOM element by document.querySelector(cssSelector) (throws error if not found)
|
|
153
|
+
* @property {HTMLElement} [element] - insert the header as a child of this given element
|
|
154
|
+
* @property {function (): HTMLElement} [elementFunction] - insert the header in to whatever element is returned from this function
|
|
155
|
+
* }
|
|
156
|
+
*
|
|
157
|
+
* @typedef GlobalEventType {
|
|
158
|
+
* @property {(e: MouseEvent) => void} globalOnClick the current event handler for global on click events
|
|
159
|
+
* @property {(e: KeyboardEvent) => void} globalOnKeydown tracks when keys are pressed down
|
|
160
|
+
* @property {(e: KeyboardEvent) => void} globalOnKeyup the current event handler for global on key press events
|
|
161
|
+
* }
|
|
162
|
+
*
|
|
163
|
+
* Partial is a `typescript` utility that takes a type and makes all its properties optional. This works in vs-code IDEs but may
|
|
164
|
+
* be problematic in other IDEs that don't support typescript "out of the box".
|
|
165
|
+
// eslint-disable-next-line jsdoc/no-undefined-types
|
|
166
|
+
* @typedef {Partial<Settings>} SettingsInput
|
|
167
|
+
*
|
|
168
|
+
* // User fields from the UtahId authority
|
|
169
|
+
* @typedef UserInfo {
|
|
170
|
+
* @property {boolean} authenticated - the current information is ratified with the authority
|
|
171
|
+
* @property {boolean | null | undefined} [disabled]
|
|
172
|
+
* @property {string | undefined} [env] - the UtahId environment
|
|
173
|
+
* @property {string | null | undefined} first - the name shown on the UtahId button when logged in
|
|
174
|
+
* @property {string | null | undefined} [id]
|
|
175
|
+
* @property {string | null | undefined} [last]
|
|
176
|
+
* @property {string[] | null | undefined} [mail]
|
|
177
|
+
* @property {string | null | undefined} [middle]
|
|
178
|
+
* @property {string | null | undefined} [status]
|
|
179
|
+
* @property {string | undefined} [type]
|
|
180
|
+
* @property {string | null | undefined} [username]
|
|
181
|
+
* }
|
|
182
|
+
*
|
|
183
|
+
* @typedef {'Automatic' | 'None' | 'Provided'} UtahIdFetchStyle
|
|
184
|
+
*
|
|
185
|
+
* @typedef UtahIdData {
|
|
186
|
+
* @property {boolean | null} isDefinitive - true when the user's state is known, false while the ajax request is inflight
|
|
187
|
+
* @property {string | null} lastError - true when the user's state is known, false while the ajax request is inflight
|
|
188
|
+
* @property {UserInfo | null} userInfo - the current logged in user info or null if not found
|
|
189
|
+
* }
|
|
190
|
+
*
|
|
191
|
+
* // only fill in what you want to change, the rest will be defaults
|
|
192
|
+
* // The UtahID header will auto fetch the user from UtahID if it is not told that your application will be controlling the signed in user
|
|
193
|
+
* // This may cause the header to jump as data comes from UtahID and your application gets data from its data source.
|
|
194
|
+
* // To prevent this jankiness, your application should call setUtahHeaderSettings with a userInfo.currentUser value of `null`. This way
|
|
195
|
+
* // the header knows not to fetch the user and your application can later call setSettings again with the current user.
|
|
196
|
+
* @typedef UtahIDSettings {
|
|
197
|
+
* @property {UserInfo | undefined | null} currentUser - null: app controls the user, undefined: header will fetch current user
|
|
198
|
+
* @property {function(UtahIdData): void | undefined} [onAuthChanged] - auth user changes, eg (newUserData) => { ... do something ... }
|
|
199
|
+
* @property {function(UIEvent): void | undefined} [onProfile] - when the UtahId's menu item for the user's profile is triggered: (e) => { }
|
|
200
|
+
* @property {function(UIEvent): void | undefined} [onSignIn] - when the UtahId button is pressed to sign in: (e) => { }
|
|
201
|
+
* @property {function(UIEvent): void | undefined} [onSignOut] - when the UtahId's menu item for sign out is triggered: (e) => { }
|
|
202
|
+
* @property {MenuItem[] | undefined} [menuItems] - menu items to add to the UtahId menu (user must be logged in to open the menu): (e) => { }
|
|
203
|
+
* }
|
|
204
|
+
*
|
|
205
|
+
* @typedef Logo {
|
|
206
|
+
* @property {HTMLElement | function(): HTMLElement} [element] an HTML Element to render
|
|
207
|
+
* @property {string | function(): string} [htmlString] string containing html that will be rendered
|
|
208
|
+
* @property {string | function(): string} [imageUrl] url to an image
|
|
209
|
+
* }
|
|
210
|
+
*
|
|
211
|
+
* @typedef FooterSettings {
|
|
212
|
+
* @property {DomLocationTarget} [domLocationTarget] - where in the DOM should the footer be inserted? (defaults to the bottom of the body)
|
|
213
|
+
* @property {boolean} [showHorizontalRule] - true to have a dividing horizontal rule placed at the top of the footer for dividing footer content
|
|
214
|
+
* }
|
|
215
|
+
*
|
|
216
|
+
* // !! Make sure to update SettingsShape in the library if this changes !!
|
|
217
|
+
* @typedef Settings {
|
|
218
|
+
* @property {ActionItem[]} [actionItems] - action items to show in the header
|
|
219
|
+
* @property {DomLocationTarget} [domLocationTarget] - where in the DOM should the header be inserted? (defaults to the top of the body)
|
|
220
|
+
* @property {FooterSettings | null} [footer] - null means to not show the footer
|
|
221
|
+
* @property {Logo} [logo] - the logo to show
|
|
222
|
+
* @property {MainMenu | false} [mainMenu] - the main menu to show on a line below the citizen experience/unbrand line
|
|
223
|
+
* @property {MediaSizes} mediaSizes - sizes for triggering media queries
|
|
224
|
+
* @property {((search: string) => void) | false} [onSearch] - if onSearch is provided, the search icon will show in the main menu bar
|
|
225
|
+
* @property {boolean} showTitle - should the title be shown (it will always be on the page for accessibility)
|
|
226
|
+
* @property {string} size - size has to be one of the `Size` types
|
|
227
|
+
* @property {string} title - the title to place at the top of the page (can be hidden) but needs to be there for accessibility
|
|
228
|
+
* @property {string} titleURL - when the agency title is triggered, the browser navigates to this url
|
|
229
|
+
* @property {UtahIDSettings | boolean} [utahId] - settings for the utahId button; true = turned on, false = turned off, object = custom
|
|
230
|
+
* }
|
|
231
|
+
*
|
|
232
|
+
*/
|
|
233
|
+
|
|
234
|
+
// without this export, `@typedef import` reports this file 'is not a module'... (눈_눈)
|
|
235
|
+
export default false;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* https://docs.joshuatz.com/cheatsheets/js/jsdoc/#non-null-assertion-in-jsdoc
|
|
3
|
+
* Exclude in JSDoc removes a type from another type. The returned `T` no longer has `null` nor `undefined` in its type
|
|
4
|
+
*
|
|
5
|
+
* @template T
|
|
6
|
+
* @param {T} value
|
|
7
|
+
* @param {string} errorMessage
|
|
8
|
+
* @returns {NonNullable<T>}
|
|
9
|
+
*/
|
|
10
|
+
export default function notNull(value, errorMessage) {
|
|
11
|
+
if (value === null || value === undefined) {
|
|
12
|
+
throw new Error(errorMessage);
|
|
13
|
+
}
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { createPopper } from '@popperjs/core';
|
|
3
|
+
import domConstants, { getCssClassSelector } from '../enumerations/domConstants';
|
|
4
|
+
import popupPlacement from '../enumerations/popupPlacement';
|
|
5
|
+
import { hideAllMenus } from '../lifecycle/globalEvents';
|
|
6
|
+
import isTouchDevice from './isTouchDevice';
|
|
7
|
+
import showHideElement from './showHideElement';
|
|
8
|
+
import checkForError from './checkForError';
|
|
9
|
+
|
|
10
|
+
/*
|
|
11
|
+
___ ___ _ _ _ _____ _____ ___ _ _ ___ _ _
|
|
12
|
+
| \ / _ \ | \| | ( ) |_ _| |_ _| / _ \ | | | | / __| | || |
|
|
13
|
+
| |) | | (_) | | .` | |/ | | | | | (_) | | |_| | | (__ | __ |
|
|
14
|
+
|___/ \___/ |_|\_| |_| |_| \___/ \___/ \___| |_||_|
|
|
15
|
+
|
|
16
|
+
If you do dare touch this, and you probably should if you need to, here are the testing scenarios to consider:
|
|
17
|
+
* have an action item (divisions) that pops open a menu with children
|
|
18
|
+
* tab through the action item so that the popup opens and tab through children so that they toggle open
|
|
19
|
+
* tabbing out of focus of the action item and children menus should then close the popup
|
|
20
|
+
* have two action items with popups (menu popup and dom content popup)
|
|
21
|
+
* click on one of the action icons to open the popup
|
|
22
|
+
* click on the other action icon to open its popup and close the other popup
|
|
23
|
+
* there was a point in time where the original popup was not closing
|
|
24
|
+
* have a header menu item with at least 3 levels deep of menu items
|
|
25
|
+
* tab to the children at the deepest level and make sure that the sub menus flyout
|
|
26
|
+
* accessibility: have an action item (divisions) that has a popup menu with children
|
|
27
|
+
* without tabbing or clicking, use accessibility (ctrl+opt+right-arrow) to move on top of the divisions action item
|
|
28
|
+
* trigger the menu (ctrl+opt+space) to have the menu toggle open
|
|
29
|
+
* do the same test as above but with the Utah ID button after logging in so that it opens the Utah Id menu
|
|
30
|
+
* have a main menu item whose children flyout and make sure hovering causes those menu items to flyout
|
|
31
|
+
* tab to a child menu in a popup menu that also has children menus
|
|
32
|
+
* this "middle" menu (has parent and children) when first focused should not have its chevron open nor show its children
|
|
33
|
+
* tabbing again to see this menu's children then opens the children and toggles the chevron
|
|
34
|
+
|
|
35
|
+
Notes:
|
|
36
|
+
* mobile devices do not have focus events. This caused issues with putting logic in focus that would work in browser but not
|
|
37
|
+
in mobile and vice/versa.
|
|
38
|
+
* when targeting items via accessibility navigation (not tab key), items do not receive focus, so focus events again were
|
|
39
|
+
problematic.
|
|
40
|
+
* Added a timeout to the open/close because the focusin, mousedown, and onclick were fighting with timing issues
|
|
41
|
+
* sometimes onfocusin would fire first, other times onmousedown would fire first
|
|
42
|
+
* and they would cause each other to see different statuses because one would open and another would close the menu
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @typedef {import('../misc/jsDocTypes').AriaHasPopupType} AriaHasPopupType
|
|
47
|
+
* @typedef {import('../misc/jsDocTypes').PopupFocusHandlerOptions} PopupFocusHandlerOptions
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Tracking focus of a wrapper that has a button for toggling and a popup inside of it
|
|
52
|
+
* for both onClicks AND tabbing to focus is pretty complicated. The idea is that while the
|
|
53
|
+
* button OR menu has focus, the menu is shown. Also global events like clicking off the wrapper
|
|
54
|
+
* or pressing escape should also close the popup.
|
|
55
|
+
*
|
|
56
|
+
* @param {HTMLElement} wrapper the wrapper containing the button and popup
|
|
57
|
+
* @param {HTMLElement} button the button that toggles the popup to open/close
|
|
58
|
+
* @param {HTMLElement} popup the actual popup being opened and closed
|
|
59
|
+
* @param {AriaHasPopupType} ariaHasPopup aria tag for popup type
|
|
60
|
+
* @param {PopupFocusHandlerOptions | undefined} options
|
|
61
|
+
*/
|
|
62
|
+
export default function popupFocusHandler(wrapper, button, popup, ariaHasPopup, options) {
|
|
63
|
+
let delayPopupTimeoutId = NaN;
|
|
64
|
+
let delayHideTimeoutId = NaN;
|
|
65
|
+
const TIMEOUT_MS_LONG = 300;
|
|
66
|
+
const TIMEOUT_MS_MEDIUM = 150;
|
|
67
|
+
const TIMEOUT_MS_SHORT = 50;
|
|
68
|
+
|
|
69
|
+
button.setAttribute('aria-expanded', 'false');
|
|
70
|
+
button.setAttribute('aria-haspopup', ariaHasPopup);
|
|
71
|
+
|
|
72
|
+
/*
|
|
73
|
+
___ ___ _ _ _ _____ _____ ___ _ _ ___ _ _
|
|
74
|
+
| \ / _ \ | \| | ( ) |_ _| |_ _| / _ \ | | | | / __| | || |
|
|
75
|
+
| |) | | (_) | | .` | |/ | | | | | (_) | | |_| | | (__ | __ |
|
|
76
|
+
|___/ \___/ |_|\_| |_| |_| \___/ \___/ \___| |_||_|
|
|
77
|
+
*/
|
|
78
|
+
/**
|
|
79
|
+
* @param {number} delayMS
|
|
80
|
+
*/
|
|
81
|
+
function performPopup(delayMS) {
|
|
82
|
+
clearTimeout(delayPopupTimeoutId);
|
|
83
|
+
clearTimeout(delayHideTimeoutId);
|
|
84
|
+
|
|
85
|
+
if (!options?.isPerformPopup || (options?.isPerformPopup && options.isPerformPopup())) {
|
|
86
|
+
delayPopupTimeoutId = window.setTimeout(
|
|
87
|
+
() => {
|
|
88
|
+
createPopper(button, popup, {
|
|
89
|
+
placement: options?.popupPlacement || popupPlacement.BOTTOM,
|
|
90
|
+
modifiers: [
|
|
91
|
+
{
|
|
92
|
+
name: 'offset',
|
|
93
|
+
options: { offset: [0, 11] },
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
});
|
|
97
|
+
showHideElement(popup, true, domConstants.POPUP__VISIBLE, domConstants.POPUP__HIDDEN);
|
|
98
|
+
button.setAttribute('aria-expanded', 'true');
|
|
99
|
+
|
|
100
|
+
// hide all tooltips
|
|
101
|
+
document.querySelectorAll(getCssClassSelector(domConstants.TOOLTIP__WRAPPER))
|
|
102
|
+
.forEach((tooltip) => tooltip.classList.add(domConstants.TOOLTIP__WRAPPER__HIDDEN));
|
|
103
|
+
},
|
|
104
|
+
delayMS
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/*
|
|
110
|
+
___ ___ _ _ _ _____ _____ ___ _ _ ___ _ _
|
|
111
|
+
| \ / _ \ | \| | ( ) |_ _| |_ _| / _ \ | | | | / __| | || |
|
|
112
|
+
| |) | | (_) | | .` | |/ | | | | | (_) | | |_| | | (__ | __ |
|
|
113
|
+
|___/ \___/ |_|\_| |_| |_| \___/ \___/ \___| |_||_|
|
|
114
|
+
*/
|
|
115
|
+
/**
|
|
116
|
+
* @param {number} delayMS
|
|
117
|
+
*/
|
|
118
|
+
function hidePopup(delayMS) {
|
|
119
|
+
clearTimeout(delayPopupTimeoutId);
|
|
120
|
+
clearTimeout(delayHideTimeoutId);
|
|
121
|
+
if (!options?.isPerformPopup || options.isPerformPopup()) {
|
|
122
|
+
delayHideTimeoutId = window.setTimeout(
|
|
123
|
+
() => {
|
|
124
|
+
showHideElement(popup, false, domConstants.POPUP__VISIBLE, domConstants.POPUP__HIDDEN);
|
|
125
|
+
button.setAttribute('aria-expanded', 'false');
|
|
126
|
+
},
|
|
127
|
+
delayMS
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/*
|
|
133
|
+
___ ___ _ _ _ _____ _____ ___ _ _ ___ _ _
|
|
134
|
+
| \ / _ \ | \| | ( ) |_ _| |_ _| / _ \ | | | | / __| | || |
|
|
135
|
+
| |) | | (_) | | .` | |/ | | | | | (_) | | |_| | | (__ | __ |
|
|
136
|
+
|___/ \___/ |_|\_| |_| |_| \___/ \___/ \___| |_||_|
|
|
137
|
+
*/
|
|
138
|
+
if (options?.preventOnClickHandling) {
|
|
139
|
+
// eslint-disable-next-line no-param-reassign
|
|
140
|
+
checkForError(!!wrapper.onclick, 'popupFocusHandler: wrapper already has an onclick');
|
|
141
|
+
// eslint-disable-next-line no-param-reassign
|
|
142
|
+
wrapper.onclick = (e) => {
|
|
143
|
+
// for menus that are both a link and have children menus, when doing a popup, prevent clicking
|
|
144
|
+
// from doing anything so that the popup menu doesn't go away on the click. The link for the
|
|
145
|
+
// menu will be a separate menu item.
|
|
146
|
+
e.preventDefault();
|
|
147
|
+
e.stopPropagation();
|
|
148
|
+
};
|
|
149
|
+
} else {
|
|
150
|
+
wrapper.addEventListener('focusin', () => performPopup(TIMEOUT_MS_MEDIUM));
|
|
151
|
+
|
|
152
|
+
wrapper.addEventListener('focusout', () => hidePopup(TIMEOUT_MS_MEDIUM));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (options?.shouldFocusOnHover) {
|
|
156
|
+
wrapper.addEventListener('mouseenter', () => performPopup(TIMEOUT_MS_LONG));
|
|
157
|
+
wrapper.addEventListener('mouseleave', () => hidePopup(TIMEOUT_MS_LONG));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/*
|
|
161
|
+
___ ___ _ _ _ _____ _____ ___ _ _ ___ _ _
|
|
162
|
+
| \ / _ \ | \| | ( ) |_ _| |_ _| / _ \ | | | | / __| | || |
|
|
163
|
+
| |) | | (_) | | .` | |/ | | | | | (_) | | |_| | | (__ | __ |
|
|
164
|
+
|___/ \___/ |_|\_| |_| |_| \___/ \___/ \___| |_||_|
|
|
165
|
+
*/
|
|
166
|
+
|
|
167
|
+
if (!options?.preventOnClickHandling) {
|
|
168
|
+
if (button.onclick) {
|
|
169
|
+
throw new Error('popupFocusHandler: button already has onclick');
|
|
170
|
+
}
|
|
171
|
+
// eslint-disable-next-line no-param-reassign
|
|
172
|
+
button.onclick = (e) => {
|
|
173
|
+
const wasAlreadyOpen = button.getAttribute('aria-expanded') === 'true';
|
|
174
|
+
// for click popups
|
|
175
|
+
if (!options?.isPerformPopup || options.isPerformPopup()) {
|
|
176
|
+
e.stopPropagation();
|
|
177
|
+
e.preventDefault();
|
|
178
|
+
/*
|
|
179
|
+
___ ___ _ _ _ _____ _____ ___ _ _ ___ _ _
|
|
180
|
+
| \ / _ \ | \| | ( ) |_ _| |_ _| / _ \ | | | | / __| | || |
|
|
181
|
+
| |) | | (_) | | .` | |/ | | | | | (_) | | |_| | | (__ | __ |
|
|
182
|
+
|___/ \___/ |_|\_| |_| |_| \___/ \___/ \___| |_||_|
|
|
183
|
+
*/
|
|
184
|
+
|
|
185
|
+
// if !wasAlreadyOpen but aria-expanded is "true" then the 'focusin' opened it (tabbed to it?)
|
|
186
|
+
if (wasAlreadyOpen && button.getAttribute('aria-expanded') === 'true') {
|
|
187
|
+
hidePopup(TIMEOUT_MS_SHORT);
|
|
188
|
+
/** @type {HTMLElement | null} */(document.activeElement)?.blur();
|
|
189
|
+
} else {
|
|
190
|
+
if (isTouchDevice()) {
|
|
191
|
+
hideAllMenus();
|
|
192
|
+
}
|
|
193
|
+
performPopup(TIMEOUT_MS_SHORT);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (options?.onClick) {
|
|
197
|
+
options.onClick(e);
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/*
|
|
203
|
+
___ ___ _ _ _ _____ _____ ___ _ _ ___ _ _
|
|
204
|
+
| \ / _ \ | \| | ( ) |_ _| |_ _| / _ \ | | | | / __| | || |
|
|
205
|
+
| |) | | (_) | | .` | |/ | | | | | (_) | | |_| | | (__ | __ |
|
|
206
|
+
|___/ \___/ |_|\_| |_| |_| \___/ \___/ \___| |_||_|
|
|
207
|
+
|
|
208
|
+
-- see top for details --
|
|
209
|
+
*/
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import isString from './isString';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Render a DOM from a string and return just the DOM child node
|
|
7
|
+
* or HTMLCollection if there is more than one Node rendered
|
|
8
|
+
* @param {string} str - HTML string to become rendered DOM
|
|
9
|
+
* @returns {HTMLCollection | Element}
|
|
10
|
+
*/
|
|
11
|
+
function renderDOM(str) {
|
|
12
|
+
const domParser = new DOMParser();
|
|
13
|
+
|
|
14
|
+
/** @type {HTMLCollection | Element} */
|
|
15
|
+
let result;
|
|
16
|
+
if (isString(str)) {
|
|
17
|
+
const rendered = /** @type {Document} */ (isString(str) ? domParser.parseFromString(str, 'text/html') : str);
|
|
18
|
+
|
|
19
|
+
const maybeResult = (
|
|
20
|
+
rendered.body.children.length > 1
|
|
21
|
+
? rendered.body.children
|
|
22
|
+
: rendered.body.children.item(0)
|
|
23
|
+
);
|
|
24
|
+
if (!maybeResult) {
|
|
25
|
+
// eslint-disable-next-line no-console
|
|
26
|
+
console.error(str);
|
|
27
|
+
throw new Error('renderDOM: nothing rendered');
|
|
28
|
+
}
|
|
29
|
+
result = maybeResult;
|
|
30
|
+
} else if (/** @type {unknown} */ (str) instanceof Element) {
|
|
31
|
+
result = str;
|
|
32
|
+
} else if (!str) {
|
|
33
|
+
throw new Error('renderDOM: falsy string passed; cannot render nothing');
|
|
34
|
+
} else {
|
|
35
|
+
// eslint-disable-next-line no-console
|
|
36
|
+
console.error(str);
|
|
37
|
+
throw new Error(`renderDOM: str is not a string nor a DOM Element : '${str}'`);
|
|
38
|
+
}
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Pull first element out of a collection if a collection was rendered
|
|
44
|
+
*
|
|
45
|
+
* @param {string | HTMLElement} str
|
|
46
|
+
* @returns {HTMLElement}
|
|
47
|
+
*/
|
|
48
|
+
export default function renderDOMSingle(str) {
|
|
49
|
+
const dom = typeof str === 'string' ? renderDOM(str) : str;
|
|
50
|
+
if (dom instanceof HTMLCollection && dom.length > 1) {
|
|
51
|
+
throw new Error('renderDOMSingle: must render a single element');
|
|
52
|
+
}
|
|
53
|
+
/** @type HTMLElement | null */
|
|
54
|
+
const firstChild = /** @type HTMLElement | null */ (dom instanceof HTMLCollection ? dom[0] : dom);
|
|
55
|
+
if (!firstChild) {
|
|
56
|
+
// eslint-disable-next-line no-console
|
|
57
|
+
console.error(str);
|
|
58
|
+
throw new Error('renderDOMSingle: nothing rendered');
|
|
59
|
+
}
|
|
60
|
+
return firstChild;
|
|
61
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {Element} element
|
|
3
|
+
* @param {boolean} isShown
|
|
4
|
+
* @param {string} visibleClass
|
|
5
|
+
* @param {string} hiddenClass
|
|
6
|
+
*/
|
|
7
|
+
export default function showHideElement(element, isShown, visibleClass, hiddenClass) {
|
|
8
|
+
if (isShown) {
|
|
9
|
+
element.classList.remove(hiddenClass);
|
|
10
|
+
element.classList.add(visibleClass);
|
|
11
|
+
} else {
|
|
12
|
+
element.classList.add(hiddenClass);
|
|
13
|
+
element.classList.remove(visibleClass);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/* eslint-disable no-bitwise */
|
|
2
|
+
|
|
3
|
+
/** https://stackoverflow.com/a/8076436/1478933
|
|
4
|
+
* @param {object | string} thing the thing to hash
|
|
5
|
+
*/
|
|
6
|
+
export default function toHash(thing) {
|
|
7
|
+
let hash = 0;
|
|
8
|
+
const string = JSON.stringify(thing);
|
|
9
|
+
for (let i = 0; i < string.length; i += 1) {
|
|
10
|
+
const code = string.charCodeAt(i);
|
|
11
|
+
hash = ((hash << 5) - hash) + code;
|
|
12
|
+
|
|
13
|
+
// Convert to 32bit integer
|
|
14
|
+
hash &= hash;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return hash;
|
|
18
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @template ValueOrFunctionValueT
|
|
3
|
+
* @param {ValueOrFunctionValueT | function(): ValueOrFunctionValueT} valueOrFunction
|
|
4
|
+
* @returns {ValueOrFunctionValueT}
|
|
5
|
+
*/
|
|
6
|
+
export default function valueOrFunctionValue(valueOrFunction) {
|
|
7
|
+
return (
|
|
8
|
+
(typeof valueOrFunction === 'function')
|
|
9
|
+
// @ts-ignore
|
|
10
|
+
? valueOrFunction()
|
|
11
|
+
: valueOrFunction
|
|
12
|
+
);
|
|
13
|
+
}
|