@internetarchive/ia-topnav 1.4.1 → 2.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.
Files changed (68) hide show
  1. package/.prettierignore +1 -1
  2. package/LICENSE +661 -661
  3. package/README.md +147 -147
  4. package/demo/index.html +28 -28
  5. package/dist/src/data/menus.js +0 -2
  6. package/dist/src/data/menus.js.map +1 -1
  7. package/dist/src/ia-topnav.d.ts +7 -10
  8. package/dist/src/ia-topnav.js +95 -133
  9. package/dist/src/ia-topnav.js.map +1 -1
  10. package/dist/src/login-button.js +17 -17
  11. package/dist/src/login-button.js.map +1 -1
  12. package/dist/src/models.d.ts +0 -4
  13. package/dist/src/models.js.map +1 -1
  14. package/dist/src/primary-nav.d.ts +5 -4
  15. package/dist/src/primary-nav.js +98 -119
  16. package/dist/src/primary-nav.js.map +1 -1
  17. package/dist/src/styles/ia-topnav.js +82 -87
  18. package/dist/src/styles/ia-topnav.js.map +1 -1
  19. package/dist/src/styles/primary-nav.js +353 -308
  20. package/dist/src/styles/primary-nav.js.map +1 -1
  21. package/dist/test/ia-topnav.test.js +27 -69
  22. package/dist/test/ia-topnav.test.js.map +1 -1
  23. package/dist/test/primary-nav.test.js +38 -9
  24. package/dist/test/primary-nav.test.js.map +1 -1
  25. package/eslint.config.mjs +53 -53
  26. package/package.json +72 -72
  27. package/prettier.config.js +9 -9
  28. package/src/data/menus.ts +650 -652
  29. package/src/ia-topnav.ts +318 -366
  30. package/src/login-button.ts +78 -78
  31. package/src/models.ts +58 -63
  32. package/src/primary-nav.ts +277 -296
  33. package/src/styles/ia-topnav.ts +85 -90
  34. package/src/styles/primary-nav.ts +356 -311
  35. package/ssl/server.key +28 -28
  36. package/test/ia-topnav.test.ts +282 -343
  37. package/test/primary-nav.test.ts +135 -94
  38. package/tsconfig.json +31 -31
  39. package/web-dev-server.config.mjs +32 -32
  40. package/web-test-runner.config.mjs +41 -41
  41. package/dist/src/lib/location-handler.d.ts +0 -1
  42. package/dist/src/lib/location-handler.js +0 -5
  43. package/dist/src/lib/location-handler.js.map +0 -1
  44. package/dist/src/nav-search.d.ts +0 -19
  45. package/dist/src/nav-search.js +0 -127
  46. package/dist/src/nav-search.js.map +0 -1
  47. package/dist/src/search-menu.d.ts +0 -20
  48. package/dist/src/search-menu.js +0 -162
  49. package/dist/src/search-menu.js.map +0 -1
  50. package/dist/src/styles/nav-search.d.ts +0 -2
  51. package/dist/src/styles/nav-search.js +0 -136
  52. package/dist/src/styles/nav-search.js.map +0 -1
  53. package/dist/src/styles/search-menu.d.ts +0 -2
  54. package/dist/src/styles/search-menu.js +0 -118
  55. package/dist/src/styles/search-menu.js.map +0 -1
  56. package/dist/test/nav-search.test.d.ts +0 -1
  57. package/dist/test/nav-search.test.js +0 -47
  58. package/dist/test/nav-search.test.js.map +0 -1
  59. package/dist/test/search-menu.test.d.ts +0 -1
  60. package/dist/test/search-menu.test.js +0 -42
  61. package/dist/test/search-menu.test.js.map +0 -1
  62. package/src/lib/location-handler.ts +0 -5
  63. package/src/nav-search.ts +0 -117
  64. package/src/search-menu.ts +0 -156
  65. package/src/styles/nav-search.ts +0 -136
  66. package/src/styles/search-menu.ts +0 -118
  67. package/test/nav-search.test.ts +0 -70
  68. package/test/search-menu.test.ts +0 -58
package/src/nav-search.ts DELETED
@@ -1,117 +0,0 @@
1
- import { nothing, html } from 'lit';
2
-
3
- import TrackedElement from './tracked-element';
4
- import navSearchCSS from './styles/nav-search';
5
- import icons from './assets/img/icons';
6
- import formatUrl from './lib/format-url';
7
- import { customElement, property, query } from 'lit/decorators.js';
8
- import { defaultTopNavConfig } from './data/menus';
9
- import { IATopNavConfig } from './models';
10
- import { iaSronlyStyles } from '@internetarchive/ia-styles';
11
-
12
- @customElement('nav-search')
13
- export class NavSearch extends TrackedElement {
14
- @property({ type: String }) baseHost = '';
15
- @property({ type: Object }) config: IATopNavConfig = defaultTopNavConfig;
16
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
17
- @property({ type: Object }) locationHandler = (url: string) => {};
18
- @property({ type: Boolean }) open = false;
19
- @property({ type: String }) openMenu = '';
20
- @property({ type: String }) searchIn = '';
21
- @property({ type: String }) searchQuery = '';
22
-
23
- @query('[name=query]') private queryInput?: HTMLInputElement;
24
-
25
- static get styles() {
26
- return [navSearchCSS, iaSronlyStyles];
27
- }
28
-
29
- search(e: CustomEvent) {
30
- const query = this.queryInput?.value;
31
-
32
- if (!query) {
33
- e.preventDefault();
34
- return false;
35
- }
36
-
37
- // TV search points to a detail page with a q param instead
38
- if (this.searchIn === 'TV') {
39
- this.locationHandler(
40
- formatUrl(`/details/tv?q=${query}` as string & Location, this.baseHost),
41
- );
42
- e.preventDefault();
43
- return false;
44
- }
45
-
46
- this.trackSubmit(e);
47
- return true;
48
- }
49
-
50
- toggleSearchMenu() {
51
- if (this.openMenu === 'search') {
52
- return;
53
- }
54
- this.dispatchEvent(
55
- new CustomEvent('menuToggled', {
56
- detail: {
57
- menuName: 'search',
58
- },
59
- composed: true,
60
- bubbles: true,
61
- }),
62
- );
63
- }
64
-
65
- get searchInsideInput() {
66
- return this.searchIn
67
- ? html`<input type="hidden" name="sin" value="${this.searchIn}" />`
68
- : nothing;
69
- }
70
-
71
- get searchEndpoint() {
72
- return '/search';
73
- }
74
-
75
- render() {
76
- const searchMenuClass = this.open ? 'flex' : 'search-inactive';
77
-
78
- return html`
79
- <div class="search-activated fade-in ${searchMenuClass}">
80
- <form
81
- id="nav-search"
82
- class="highlight"
83
- .action=${formatUrl(
84
- this.searchEndpoint as string & Location,
85
- this.baseHost,
86
- )}
87
- method="get"
88
- @submit=${this.search}
89
- data-event-submit-tracking="${this.config
90
- ?.eventCategory}|NavSearchSubmit"
91
- >
92
- <label for="query" class="sr-only">Search the Archive</label>
93
- <input
94
- type="text"
95
- name="query"
96
- id="query"
97
- class="search-field"
98
- placeholder="Search"
99
- autocomplete="off"
100
- value=${this.searchQuery || ''}
101
- @focus=${this.toggleSearchMenu}
102
- />
103
- ${this.searchInsideInput}
104
- <button
105
- type="submit"
106
- class="search"
107
- tabindex="-1"
108
- data-event-click-tracking="${this.config
109
- ?.eventCategory}|NavSearchClose"
110
- >
111
- ${icons.search}
112
- </button>
113
- </form>
114
- </div>
115
- `;
116
- }
117
- }
@@ -1,156 +0,0 @@
1
- import { html } from 'lit';
2
- import { customElement, property } from 'lit/decorators.js';
3
-
4
- import TrackedElement from './tracked-element';
5
- import searchMenuCSS from './styles/search-menu';
6
- import formatUrl from './lib/format-url';
7
- import { IATopNavConfig } from './models';
8
- import { defaultTopNavConfig } from './data/menus';
9
-
10
- @customElement('search-menu')
11
- export class SearchMenu extends TrackedElement {
12
- @property({ type: String }) baseHost = '';
13
- @property({ type: Object }) config: IATopNavConfig = defaultTopNavConfig;
14
- @property({ type: Boolean }) hideSearch = false;
15
- @property({ type: String }) openMenu = '';
16
- @property({ type: Boolean }) searchMenuOpen = false;
17
- @property({ type: Boolean }) searchMenuAnimate = false;
18
- @property({ type: String }) selectedSearchType = '';
19
-
20
- static get styles() {
21
- return searchMenuCSS;
22
- }
23
-
24
- firstUpdated() {
25
- this.shadowRoot?.addEventListener('keydown', (e) =>
26
- this.handleKeyDownEvent(e as KeyboardEvent),
27
- );
28
- }
29
-
30
- disconnectedCallback() {
31
- // Clean up event listener when the element is removed
32
- this.shadowRoot?.removeEventListener('keydown', (e) =>
33
- this.handleKeyDownEvent(e as KeyboardEvent),
34
- );
35
- }
36
-
37
- private handleKeyDownEvent(e: KeyboardEvent) {
38
- if (!this.shadowRoot) return;
39
-
40
- const searchTypes = this.shadowRoot.querySelectorAll(
41
- '.search-menu-inner label input[type=radio]',
42
- ) as NodeListOf<HTMLInputElement>;
43
-
44
- const length = searchTypes.length - 1;
45
- if (!length) return;
46
-
47
- const searchTypeHandler = (index: number) => {
48
- e.preventDefault();
49
- const searchType = searchTypes[index];
50
- searchType.checked = true;
51
- searchType.dispatchEvent(new Event('change'));
52
- searchType.focus();
53
- };
54
-
55
- if (e.key === 'Home') {
56
- searchTypeHandler(0);
57
- } else if (e.key === 'End') {
58
- searchTypeHandler(length);
59
- }
60
- }
61
-
62
- selectSearchType(e: Event) {
63
- const target = e.target as HTMLInputElement;
64
- this.selectedSearchType = target.value;
65
- }
66
-
67
- searchInChanged(e: InputEvent) {
68
- const target = e.target as HTMLInputElement;
69
- this.dispatchEvent(
70
- new CustomEvent('searchInChanged', {
71
- detail: {
72
- searchIn: target.value,
73
- },
74
- }),
75
- );
76
- }
77
-
78
- get searchTypesTemplate() {
79
- const searchTypes = [
80
- {
81
- label: 'metadata',
82
- value: '',
83
- isDefault: true,
84
- },
85
- {
86
- label: 'text contents',
87
- value: 'TXT',
88
- },
89
- {
90
- label: 'TV news captions',
91
- value: 'TV',
92
- },
93
- {
94
- label: 'radio transcripts',
95
- value: 'RADIO',
96
- },
97
- {
98
- label: 'archived web sites',
99
- value: 'WEB',
100
- },
101
- ].map(({ value, label, isDefault }) => {
102
- if (
103
- this.config.hiddenSearchOptions &&
104
- this.config.hiddenSearchOptions.includes(value)
105
- ) {
106
- return html``;
107
- }
108
- return html`
109
- <label @click="${this.selectSearchType}">
110
- <input
111
- form="nav-search"
112
- type="radio"
113
- name="sin"
114
- value="${value}"
115
- ?checked=${isDefault}
116
- ?disabled=${!this.openMenu}
117
- @change=${this.searchInChanged}
118
- />
119
- Search ${label}
120
- </label>
121
- `;
122
- });
123
-
124
- return searchTypes;
125
- }
126
-
127
- get menuClass() {
128
- return this.openMenu === 'search' ? 'open' : 'closed';
129
- }
130
-
131
- render() {
132
- if (this.hideSearch) {
133
- return html``;
134
- }
135
-
136
- return html`
137
- <div class="menu-wrapper">
138
- <div
139
- class="search-menu-inner tx-slide ${this.menuClass}"
140
- aria-hidden="${!this.openMenu}"
141
- >
142
- ${this.searchTypesTemplate}
143
- <a
144
- class="advanced-search"
145
- href="${formatUrl('/advancedsearch.php', this.baseHost)}"
146
- @click=${this.trackClick}
147
- tabindex=${this.openMenu ? '0' : '-1'}
148
- data-event-click-tracking="${this.config
149
- .eventCategory}|NavAdvancedSearch"
150
- >Advanced Search</a
151
- >
152
- </div>
153
- </div>
154
- `;
155
- }
156
- }
@@ -1,136 +0,0 @@
1
- import { css } from 'lit';
2
-
3
- export default css`
4
- input[type='text'] {
5
- color: var(--grey13);
6
- }
7
-
8
- input:focus {
9
- outline: none;
10
- }
11
- button {
12
- background: none;
13
- color: inherit;
14
- border: none;
15
- font: inherit;
16
- cursor: pointer;
17
- }
18
- button:focus {
19
- outline: none;
20
- }
21
- .search {
22
- padding-top: 0;
23
- margin-right: 0.5rem;
24
- }
25
- .search svg {
26
- position: relative;
27
- fill: var(--activeSearchColor);
28
- }
29
- .search-activated {
30
- display: -webkit-box;
31
- display: -ms-flexbox;
32
- display: flex;
33
- position: absolute;
34
- top: 0;
35
- right: 4rem;
36
- bottom: 0;
37
- left: 4rem;
38
- z-index: 3;
39
- padding: 0.5rem 0.2rem;
40
- border-radius: 1rem 1rem 0 0;
41
- background: var(--searchActiveBg);
42
- }
43
- .search-inactive {
44
- display: none;
45
- }
46
- .search-activated .highlight,
47
- .search-activated .search {
48
- background: var(--searchActiveInputBg);
49
- border-radius: 0.5rem;
50
- }
51
- .search-activated .highlight {
52
- display: -webkit-box;
53
- display: -ms-flexbox;
54
- display: flex;
55
- width: 100%;
56
- margin: 0 0.5rem;
57
- }
58
- .search-activated .search {
59
- height: 100%;
60
- padding: 0;
61
- margin-right: 0;
62
- -ms-flex-item-align: center;
63
- -ms-grid-row-align: center;
64
- align-self: center;
65
- }
66
- .search-activated .search svg {
67
- height: 3rem;
68
- width: 3rem;
69
- }
70
- .search-activated .search-field {
71
- width: 100%;
72
- height: 100%;
73
- box-sizing: border-box;
74
- padding-left: 1rem;
75
- border-radius: 0.5rem;
76
- border: none;
77
- font-size: 1.6rem;
78
- text-align: center;
79
- }
80
- .search-activated .search-field:focus {
81
- outline: none;
82
- }
83
- @keyframes fade-in {
84
- 0% {
85
- opacity: 0;
86
- }
87
- 100% {
88
- opacity: 1;
89
- }
90
- }
91
- .fade-in {
92
- animation: fade-in 0.2s forwards;
93
- }
94
-
95
- @media (min-width: 890px) {
96
- .search svg {
97
- display: inline;
98
- width: 2.8rem;
99
- height: 2.8rem;
100
- vertical-align: -14px;
101
- }
102
- .search path {
103
- fill: var(--desktopSearchIconFill);
104
- }
105
-
106
- .search-inactive,
107
- .search-activated {
108
- display: block;
109
- position: static;
110
- padding: 1.1rem 0.2rem;
111
- background: transparent;
112
- }
113
-
114
- .search-activated .highlight {
115
- width: 13rem;
116
- height: 2.8rem;
117
- -webkit-box-orient: horizontal;
118
- -webkit-box-direction: reverse;
119
- -ms-flex-direction: row-reverse;
120
- flex-direction: row-reverse;
121
- }
122
-
123
- .search-activated .search-field {
124
- width: calc(100% - 28px);
125
- height: 100%;
126
- padding-left: 0;
127
- font-size: 1.4rem;
128
- text-align: left;
129
- }
130
-
131
- .search-activated .search svg {
132
- width: 2.8rem;
133
- height: 2.8rem;
134
- }
135
- }
136
- `;
@@ -1,118 +0,0 @@
1
- import { css } from 'lit';
2
-
3
- export default css`
4
- .menu-wrapper {
5
- position: relative;
6
- }
7
-
8
- button:focus,
9
- input:focus {
10
- outline-color: var(--linkColor);
11
- outline-width: 0.16rem;
12
- outline-style: auto;
13
- outline-offset: 2px !important;
14
- }
15
- .search-menu-inner {
16
- position: absolute;
17
- right: 0;
18
- left: 0;
19
- z-index: 4;
20
- padding: 0 4.5rem;
21
- font-size: 1.6rem;
22
- background-color: var(--searchMenuBg);
23
- }
24
- .tx-slide {
25
- overflow: hidden;
26
- transition-property: top;
27
- transition-duration: 0.2s;
28
- transition-timing-function: ease;
29
- }
30
- .initial,
31
- .closed {
32
- top: var(--topOffset, -1500px);
33
- }
34
- .closed {
35
- transition-duration: 0.2s;
36
- }
37
-
38
- label,
39
- a {
40
- padding: 1rem;
41
- display: block;
42
- }
43
-
44
- .advanced-search {
45
- text-decoration: underline;
46
- color: inherit;
47
- line-height: normal;
48
- padding: 0.5rem;
49
- margin-top: 5px;
50
- }
51
-
52
- .advanced-search:hover {
53
- text-decoration: none;
54
- }
55
-
56
- @media (min-width: 890px) {
57
- .search-menu-inner {
58
- overflow: visible;
59
- right: 2rem;
60
- left: auto;
61
- z-index: 5;
62
- padding: 1rem 2rem;
63
- transition: opacity 0.2s ease-in-out;
64
- font-size: 1.4rem;
65
- color: var(--inverseTextColor);
66
- border-radius: 2px;
67
- background: var(--primaryTextColor);
68
- box-shadow: 0 1px 2px 1px rgba(0, 0, 0, 0.15);
69
- }
70
-
71
- .search-menu-inner:after {
72
- position: absolute;
73
- right: 7px;
74
- top: -7px;
75
- width: 12px;
76
- height: 7px;
77
- box-sizing: border-box;
78
- color: #fff;
79
- content: '';
80
- border-bottom: 7px solid currentColor;
81
- border-left: 6px solid transparent;
82
- border-right: 6px solid transparent;
83
- }
84
-
85
- .advanced-search {
86
- text-decoration: none;
87
- color: var(--linkColor);
88
- }
89
-
90
- .advanced-search:hover {
91
- text-decoration: underline;
92
- }
93
-
94
- .initial,
95
- .closed {
96
- opacity: 0;
97
- transition-duration: 0.2s;
98
- }
99
-
100
- .open {
101
- opacity: 1;
102
- }
103
-
104
- label {
105
- padding: 0;
106
- font-weight: normal;
107
- margin: 0;
108
- }
109
-
110
- label + label {
111
- padding-top: 7px;
112
- }
113
-
114
- a {
115
- padding: 1rem 0 0 0;
116
- }
117
- }
118
- `;
@@ -1,70 +0,0 @@
1
- import { html, fixture, expect } from '@open-wc/testing';
2
- import sinon from 'sinon';
3
-
4
- import '../src/nav-search';
5
- import { NavSearch } from '../src/nav-search';
6
-
7
- const component = html`<nav-search></nav-search>`;
8
-
9
- describe('<nav-search>', () => {
10
- it('defaults to closed', async () => {
11
- const el = await fixture<NavSearch>(component);
12
-
13
- expect(el.open).to.be.false;
14
- });
15
-
16
- it('does not allow search form to submit if query empty', async () => {
17
- const el = await fixture<NavSearch>(component);
18
-
19
- // const result = el.search({
20
- // preventDefault: () => {},
21
- // });
22
-
23
- // expect(result).to.be.false;
24
- });
25
-
26
- it('redirects to the TV details page when search inside is TV', async () => {
27
- const query = 'bananas';
28
- const submitEvent = {
29
- type: 'submit',
30
- preventDefault: () => {},
31
- };
32
- const locationHandler = sinon.fake();
33
- const el = await fixture<NavSearch>(
34
- html`<nav-search .locationHandler=${locationHandler}></nav-search>`,
35
- );
36
-
37
- el.searchIn = 'TV';
38
-
39
- await el.updateComplete;
40
-
41
- // el.shadowRoot.querySelector('[name=query]').value = query;
42
- // el.search(submitEvent);
43
-
44
- // expect(locationHandler.callCount).to.equal(1);
45
- // expect(locationHandler.firstArg).to.contain(`/details/tv?q=${query}`);
46
- });
47
-
48
- it('prefills the search query when present in config', async () => {
49
- const el = await fixture<NavSearch>(
50
- html`<nav-search searchQuery="bananas"></nav-search>`,
51
- );
52
-
53
- const queryInput = el.shadowRoot?.querySelector(
54
- '[name="query"]',
55
- ) as HTMLInputElement;
56
-
57
- expect(queryInput.value).to.equal('bananas');
58
- });
59
-
60
- it('conditionally renders `sin` input based on `searchIn` truthiness', async () => {
61
- const el = await fixture<NavSearch>(component);
62
-
63
- expect(el.shadowRoot?.querySelector('[name="sin"]')).to.be.null;
64
-
65
- el.searchIn = 'TV';
66
- await el.updateComplete;
67
-
68
- expect(el.shadowRoot?.querySelector('[name="sin"]')).not.to.be.null;
69
- });
70
- });
@@ -1,58 +0,0 @@
1
- import { html, fixture, expect } from '@open-wc/testing';
2
-
3
- import '../src/search-menu';
4
- import { SearchMenu } from '../src/search-menu';
5
-
6
- const component = html`<search-menu></search-menu>`;
7
-
8
- describe('<search-menu>', () => {
9
- it('sets default properties', async () => {
10
- const el = await fixture<SearchMenu>(component);
11
-
12
- expect(el.searchMenuOpen).to.be.false;
13
- expect(el.searchMenuAnimate).to.be.false;
14
- expect(el.selectedSearchType).to.equal('');
15
- });
16
-
17
- it('sets selected search type', async () => {
18
- const el = await fixture<SearchMenu>(component);
19
- const value = 'text';
20
-
21
- const inputEvent = new InputEvent('input');
22
- Object.defineProperty(inputEvent, 'target', {
23
- value: {
24
- value,
25
- },
26
- writable: false,
27
- });
28
-
29
- el.selectSearchType(inputEvent);
30
-
31
- expect(el.selectedSearchType).to.equal(value);
32
- });
33
-
34
- it('renders with closed class if done animating', async () => {
35
- const el = await fixture<SearchMenu>(component);
36
-
37
- el.searchMenuAnimate = true;
38
- await el.updateComplete;
39
-
40
- expect(
41
- el.shadowRoot
42
- ?.querySelector('.search-menu-inner')
43
- ?.classList.contains('closed'),
44
- ).to.be.true;
45
- });
46
-
47
- it('omits rendering of an option when hiddenSearchOptions has a value', async () => {
48
- const el = await fixture<SearchMenu>(component);
49
- const hiddenSearchOptions = ['WEB', 'RADIO'];
50
-
51
- el.config = { hiddenSearchOptions };
52
- await el.updateComplete;
53
-
54
- hiddenSearchOptions.forEach((value) => {
55
- expect(el.shadowRoot?.querySelector(`[value=${value}]`)).to.equal(null);
56
- });
57
- });
58
- });