diff --git a/app/assets/stylesheets/components/_step-indicator.scss b/app/assets/stylesheets/components/_step-indicator.scss
index d002d1cf2c9..1e5a1efb012 100644
--- a/app/assets/stylesheets/components/_step-indicator.scss
+++ b/app/assets/stylesheets/components/_step-indicator.scss
@@ -2,7 +2,8 @@ $step-indicator-current-step-border-width: 3px;
$step-indicator-line-height: 4px;
$step-indicator-pending-color: #a8b6c6;
-.step-indicator {
+lg-step-indicator {
+ display: block;
border-bottom: 1px solid color('primary-light');
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.1);
margin-bottom: units(4);
diff --git a/spec/javascripts/packages/step-indicator/index-spec.js b/app/javascript/packages/step-indicator/index.spec.ts
similarity index 76%
rename from spec/javascripts/packages/step-indicator/index-spec.js
rename to app/javascript/packages/step-indicator/index.spec.ts
index 1b8278d27c9..45a64266345 100644
--- a/spec/javascripts/packages/step-indicator/index-spec.js
+++ b/app/javascript/packages/step-indicator/index.spec.ts
@@ -1,15 +1,21 @@
-import StepIndicator from '@18f/identity-step-indicator';
+import sinon from 'sinon';
import userEvent from '@testing-library/user-event';
-import { useSandbox } from '../../support/sinon';
-import useDefineProperty from '../../support/define-property';
+import { useDefineProperty } from '@18f/identity-test-helpers';
+import StepIndicator from './index';
describe('StepIndicator', () => {
- const sandbox = useSandbox();
+ const sandbox = sinon.createSandbox();
const defineProperty = useDefineProperty();
+ before(() => {
+ if (!customElements.get('lg-step-indicator')) {
+ customElements.define('lg-step-indicator', StepIndicator);
+ }
+ });
+
function initialize({ currentStepIndex = 0 } = {}) {
document.body.innerHTML = `
-
+
${Array.from(Array(5))
.map(
@@ -25,12 +31,20 @@ describe('StepIndicator', () => {
)
.join('')}
- `;
- const stepIndicator = new StepIndicator(document.body.firstElementChild);
- stepIndicator.bind();
- return stepIndicator;
+ `;
+ return document.querySelector('lg-step-indicator') as StepIndicator;
}
+ it('cleans up event listeners', () => {
+ window.resizeTo(1024, 768);
+ initialize();
+ sandbox.spy(HTMLElement.prototype, 'setAttribute');
+ document.body.innerHTML = '';
+ window.resizeTo(340, 600);
+
+ expect(HTMLElement.prototype.setAttribute).not.to.have.been.called();
+ });
+
context('small viewport', () => {
beforeEach(() => {
window.resizeTo(340, 600);
@@ -43,9 +57,12 @@ describe('StepIndicator', () => {
});
it('scrolls to current item', () => {
- sandbox.stub(window, 'getComputedStyle').callsFake((element) => ({
- paddingLeft: element.classList.contains('step-indicator__scroller') ? '24px' : '0',
- }));
+ sandbox.stub(window, 'getComputedStyle').callsFake(
+ (element) =>
+ ({
+ paddingLeft: element.classList.contains('step-indicator__scroller') ? '24px' : '0',
+ } as CSSStyleDeclaration),
+ );
defineProperty(window.Element.prototype, 'scrollWidth', {
get() {
return this.classList.contains('step-indicator__scroller') ? 593 : 0;
diff --git a/app/javascript/packages/step-indicator/index.js b/app/javascript/packages/step-indicator/index.ts
similarity index 71%
rename from app/javascript/packages/step-indicator/index.js
rename to app/javascript/packages/step-indicator/index.ts
index 22245c43059..c75e9dc08d0 100644
--- a/app/javascript/packages/step-indicator/index.js
+++ b/app/javascript/packages/step-indicator/index.ts
@@ -1,36 +1,42 @@
const SMALL_VIEWPORT_MEDIA_QUERY = '(max-width: 639px)';
-class StepIndicator {
- /**
- * @param {HTMLElement} wrapper
- */
- constructor(wrapper) {
- this.elements = {
- wrapper,
- scroller: /** @type {HTMLElement} */ (wrapper.querySelector('.step-indicator__scroller')),
- currentStep: /** @type {HTMLElement?} */ (
- wrapper.querySelector('.step-indicator__step--current')
- ),
- };
- }
+interface StepIndicatorElements {
+ scroller: HTMLElement;
+
+ currentStep: HTMLElement | null;
+}
+
+class StepIndicator extends HTMLElement {
+ elements: StepIndicatorElements;
+
+ mediaQueryList: MediaQueryList | null;
get isSmallViewport() {
return this.mediaQueryList ? this.mediaQueryList.matches : false;
}
- bind() {
+ connectedCallback() {
+ this.elements = {
+ scroller: this.querySelector('.step-indicator__scroller')!,
+ currentStep: this.querySelector('.step-indicator__step--current'),
+ };
+
this.mediaQueryList = window.matchMedia(SMALL_VIEWPORT_MEDIA_QUERY);
- this.mediaQueryList.addListener(() => this.onBreakpointMatchChange());
+ this.mediaQueryList.addListener(this.onBreakpointMatchChange);
this.onBreakpointMatchChange();
if (this.isSmallViewport) {
this.setScrollOffset();
}
}
- onBreakpointMatchChange() {
- this.toggleWrapperFocusable();
+ disconnectedCallback() {
+ this.mediaQueryList?.removeListener(this.onBreakpointMatchChange);
}
+ onBreakpointMatchChange = () => {
+ this.toggleWrapperFocusable();
+ };
+
setScrollOffset() {
const { currentStep, scroller } = this.elements;
if (currentStep) {
diff --git a/app/javascript/packages/test-helpers/README.md b/app/javascript/packages/test-helpers/README.md
new file mode 100644
index 00000000000..76b55e0fb40
--- /dev/null
+++ b/app/javascript/packages/test-helpers/README.md
@@ -0,0 +1,3 @@
+# `@18f/identity-test-helpers`
+
+Convenient utilities and Mocha lifecycle helpers for common test implementations.
diff --git a/app/javascript/packages/test-helpers/index.ts b/app/javascript/packages/test-helpers/index.ts
index 0182e8170ff..c835adac397 100644
--- a/app/javascript/packages/test-helpers/index.ts
+++ b/app/javascript/packages/test-helpers/index.ts
@@ -1 +1,2 @@
+export { default as useDefineProperty } from './use-define-property';
export { default as usePropertyValue } from './use-property-value';
diff --git a/app/javascript/packages/test-helpers/use-define-property.spec.ts b/app/javascript/packages/test-helpers/use-define-property.spec.ts
new file mode 100644
index 00000000000..92740e42bc3
--- /dev/null
+++ b/app/javascript/packages/test-helpers/use-define-property.spec.ts
@@ -0,0 +1,19 @@
+import useDefineProperty from './use-define-property';
+
+describe('useDefineProperty', () => {
+ const defineProperty = useDefineProperty();
+
+ before(() => {
+ (global as any).useDefineProperty = 10;
+ defineProperty(global, 'useDefineProperty', { get: () => 20 });
+ });
+
+ after(() => {
+ expect((global as any).useDefineProperty).to.equal(10);
+ delete (global as any).useDefineProperty;
+ });
+
+ it('has property descriptor during spec', () => {
+ expect((global as any).useDefineProperty).to.equal(20);
+ });
+});
diff --git a/spec/javascripts/support/define-property.js b/app/javascript/packages/test-helpers/use-define-property.ts
similarity index 59%
rename from spec/javascripts/support/define-property.js
rename to app/javascript/packages/test-helpers/use-define-property.ts
index 0b6de51756c..db60a6eceac 100644
--- a/spec/javascripts/support/define-property.js
+++ b/app/javascript/packages/test-helpers/use-define-property.ts
@@ -1,11 +1,11 @@
+type RedefinedProperty = [any, PropertyKey, PropertyDescriptor | undefined];
+
/**
* A proxy to Object.defineProperty to use in redefining an existing object and reverting that
* definition to its original value after the test has completed.
- *
- * @return {ObjectConstructor['defineProperty']}
*/
-export default function useDefineProperty() {
- let redefined = [];
+function useDefineProperty(): ObjectConstructor['defineProperty'] {
+ let redefined: Array = [];
afterEach(() => {
redefined.forEach(([object, property, originalDescriptor]) => {
@@ -18,9 +18,15 @@ export default function useDefineProperty() {
redefined = [];
});
- return function defineProperty(object, property, descriptor) {
+ return function defineProperty(
+ object: O,
+ property: PropertyKey,
+ descriptor: PropertyDescriptor,
+ ) {
const originalDescriptor = Object.getOwnPropertyDescriptor(object, property);
redefined.push([object, property, originalDescriptor]);
- Object.defineProperty(object, property, descriptor);
+ return Object.defineProperty(object, property, descriptor);
};
}
+
+export default useDefineProperty;
diff --git a/app/javascript/packages/test-helpers/use-property-value.spec.ts b/app/javascript/packages/test-helpers/use-property-value.spec.ts
index 820f3395673..173bc970dbf 100644
--- a/app/javascript/packages/test-helpers/use-property-value.spec.ts
+++ b/app/javascript/packages/test-helpers/use-property-value.spec.ts
@@ -1,15 +1,15 @@
import usePropertyValue from './use-property-value';
describe('usePropertyValue', () => {
- (global as any).foo = 10;
- usePropertyValue(global as any, 'foo', 20);
+ (global as any).usePropertyValue = 10;
+ usePropertyValue(global as any, 'usePropertyValue', 20);
after(() => {
- expect((global as any).foo).to.equal(10);
- delete (global as any).foo;
+ expect((global as any).usePropertyValue).to.equal(10);
+ delete (global as any).usePropertyValue;
});
it('has value during spec', () => {
- expect((global as any).foo).to.equal(20);
+ expect((global as any).usePropertyValue).to.equal(20);
});
});
diff --git a/app/javascript/packs/step-indicator.js b/app/javascript/packs/step-indicator.js
index d30b1ff2aaf..48ca98bfb75 100644
--- a/app/javascript/packs/step-indicator.js
+++ b/app/javascript/packs/step-indicator.js
@@ -1,4 +1,3 @@
import StepIndicator from '@18f/identity-step-indicator';
-const wrappers = Array.from(document.querySelectorAll('.step-indicator'));
-wrappers.forEach((wrapper) => new StepIndicator(/** @type {HTMLElement} */ (wrapper)).bind());
+customElements.define('lg-step-indicator', StepIndicator);
diff --git a/app/views/shared/_step_indicator.html.erb b/app/views/shared/_step_indicator.html.erb
index c61615645c9..57257457882 100644
--- a/app/views/shared/_step_indicator.html.erb
+++ b/app/views/shared/_step_indicator.html.erb
@@ -5,9 +5,7 @@ locals:
* locale_scope: Scope under which to find title translations for steps given as names.
* current_step: Current step.
%>
-<% classes = ['step-indicator']
- classes << local_assigns[:class] if local_assigns[:class]
- normalized_steps = steps.map do |step|
+<% normalized_steps = steps.map do |step|
if local_assigns[:locale_scope]
{ title: t(step[:name], scope: [:step_indicator, :flows, locale_scope]) }.merge(step)
else
@@ -16,10 +14,11 @@ locals:
end
current_step_index = normalized_steps.index { |step| step[:name] == local_assigns[:current_step] } %>
-<%= tag.div(
+<%= content_tag(
+ :'lg-step-indicator',
role: 'region',
aria: { label: t('step_indicator.accessible_label') },
- class: classes,
+ class: local_assigns[:class],
) do %>
<% normalized_steps.each_with_index do |step, index| %>
diff --git a/spec/javascripts/packs/form-steps-wait-spec.js b/spec/javascripts/packs/form-steps-wait-spec.js
index e495438ddc6..0175a857274 100644
--- a/spec/javascripts/packs/form-steps-wait-spec.js
+++ b/spec/javascripts/packs/form-steps-wait-spec.js
@@ -1,6 +1,6 @@
import { fireEvent, findByRole } from '@testing-library/dom';
+import { useDefineProperty } from '@18f/identity-test-helpers';
import { useSandbox } from '../support/sinon';
-import useDefineProperty from '../support/define-property';
import {
FormStepsWait,
getDOMFromHTML,
diff --git a/spec/javascripts/packs/webauthn-setup-spec.js b/spec/javascripts/packs/webauthn-setup-spec.js
index df52adb6f2d..3ed12e62b7d 100644
--- a/spec/javascripts/packs/webauthn-setup-spec.js
+++ b/spec/javascripts/packs/webauthn-setup-spec.js
@@ -1,5 +1,5 @@
+import { useDefineProperty } from '@18f/identity-test-helpers';
import { useSandbox } from '../support/sinon';
-import useDefineProperty from '../support/define-property';
import { reloadWithError } from '../../../app/javascript/packs/webauthn-setup';
describe('webauthn-setup', () => {
diff --git a/spec/javascripts/packs/webauthn-unhide-spec.js b/spec/javascripts/packs/webauthn-unhide-spec.js
index d1619e344fa..aec71ed128b 100644
--- a/spec/javascripts/packs/webauthn-unhide-spec.js
+++ b/spec/javascripts/packs/webauthn-unhide-spec.js
@@ -1,6 +1,6 @@
import { screen } from '@testing-library/dom';
+import { useDefineProperty } from '@18f/identity-test-helpers';
import { useSandbox } from '../support/sinon';
-import useDefineProperty from '../support/define-property';
import { unhideWebauthn } from '../../../app/javascript/packs/webauthn-unhide';
describe('webauthn-unhide', () => {
diff --git a/spec/views/shared/_step_indicator.html.erb_spec.rb b/spec/views/shared/_step_indicator.html.erb_spec.rb
index 8ad93d62e1b..264276098b1 100644
--- a/spec/views/shared/_step_indicator.html.erb_spec.rb
+++ b/spec/views/shared/_step_indicator.html.erb_spec.rb
@@ -44,8 +44,8 @@
context 'without custom classes given' do
let(:classes) { nil }
- it 'renders with default classes' do
- expect(rendered).to have_selector('.step-indicator')
+ it 'renders with default tag' do
+ expect(rendered).to have_selector('lg-step-indicator')
end
end
@@ -53,7 +53,7 @@
let(:classes) { 'my-custom-class' }
it 'renders with additional custom classes' do
- expect(rendered).to have_selector('.step-indicator.my-custom-class')
+ expect(rendered).to have_selector('lg-step-indicator.my-custom-class')
end
end
end