Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: Convert to JSX (fixes #238) #250

Merged
merged 26 commits into from
Mar 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
25bf3d9
Create initial JSX file, change view template filename
swashbuck Feb 16, 2023
5450786
Move click events
swashbuck Feb 21, 2023
c6c1369
Fix item width
swashbuck Feb 21, 2023
35cd96b
Merge remote-tracking branch 'origin/master' into issue/238
swashbuck Feb 21, 2023
62db272
Fix strapline widths
swashbuck Feb 21, 2023
871b4d5
Remove click events from progress dots
swashbuck Feb 21, 2023
3cf6744
Item title aria-level
swashbuck Feb 21, 2023
3c781bc
Normalize graphic alt text
swashbuck Feb 21, 2023
3797646
Move mode classes to JSX
swashbuck Feb 21, 2023
24519f7
Move items-are-full-width to JSX, remove unused disable-animation class
swashbuck Feb 21, 2023
5523c0b
Active states on progress dots
swashbuck Feb 22, 2023
ec01b7c
Move transform styles from moveSliderToIndex() to JSX
swashbuck Feb 22, 2023
e0af34f
Button enable/disable, aria-labels
swashbuck Mar 3, 2023
38b145b
Content and image active visibility
swashbuck Mar 3, 2023
2dda047
Replace user-drag CSS property with draggable HTML property
swashbuck Mar 3, 2023
57ecec7
Remove onItemsVisitedChange()
swashbuck Mar 3, 2023
4b3695e
Add visibility hidden to narrative__content-item
swashbuck Mar 3, 2023
18d4531
Focus functionality
swashbuck Mar 3, 2023
6a9decd
Remove comments
swashbuck Mar 3, 2023
1cc7ee9
Instruction error styles
swashbuck Mar 3, 2023
b015a5e
Check for element before trying to focus
swashbuck Mar 8, 2023
6c456b5
Check prefers-reduced-motion preference for transitions
swashbuck Mar 8, 2023
c64d2f4
Return early from focusOnNarrativeElement()
swashbuck Mar 10, 2023
2905141
Use ref for focus in focusOnNarrativeElement
swashbuck Mar 13, 2023
901d019
Remove error styles, change class to 'has-error' and move to narrativ…
swashbuck Mar 13, 2023
f96a583
Instruction error fixes
swashbuck Mar 13, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 76 additions & 106 deletions js/NarrativeView.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
import Adapt from 'core/js/adapt';
import components from 'core/js/components';
import a11y from 'core/js/a11y';
import device from 'core/js/device';
import notify from 'core/js/notify';
import ComponentView from 'core/js/views/componentView';
import MODE from './modeEnum';
import { compile } from 'core/js/reactHelpers';

class NarrativeView extends ComponentView {

events() {
return {
'click .js-narrative-strapline-open-popup': 'openPopup',
'click .js-narrative-controls-click': 'onNavigationClicked',
'click .js-narrative-progress-click': 'onProgressClicked',
'swipeleft .js-narrative-swipe': 'onSwipeLeft',
'swiperight .js-narrative-swipe': 'onSwipeRight'
};
Expand All @@ -21,7 +18,10 @@ class NarrativeView extends ComponentView {
initialize(...args) {
super.initialize(...args);

this._isInitial = true;
this.model.set('_isInitial', true);
this.model.set('_activeItemIndex', 0);
this.onNavigationClicked = this.onNavigationClicked.bind(this);
this.openPopup = this.openPopup.bind(this);
}

preRender() {
Expand All @@ -32,63 +32,37 @@ class NarrativeView extends ComponentView {
this.renderMode();

this.listenTo(this.model.getChildren(), {
'change:_isActive': this.onItemsActiveChange,
'change:_isVisited': this.onItemsVisitedChange
'change:_isActive': this.onItemsActiveChange
});

this.calculateWidths();
}

onItemsActiveChange(item, _isActive) {
if (!_isActive) return;

if (this.isTextBelowImage()) {
item.toggleVisited(true);
}
this.setStage(item);
this.setFocus(item.get('_index'));
}

setFocus(itemIndex) {
if (this._isInitial) return;
const $straplineHeaderElm = this.$('.narrative__strapline-header-inner');
const hasStraplineTransition = !this.isLargeMode() && ($straplineHeaderElm.css('transitionDuration') !== '0s');
if (hasStraplineTransition) {
$straplineHeaderElm.one('transitionend', () => {
this.focusOnNarrativeElement(itemIndex);
});
return;
}

this.focusOnNarrativeElement(itemIndex);
}

focusOnNarrativeElement(itemIndex) {
const dataIndexAttr = `[data-index='${itemIndex}']`;
const $elementToFocus = this.isLargeMode() ?
this.$(`.narrative__content-item${dataIndexAttr}`) :
this.$(`.narrative__strapline-btn${dataIndexAttr}`);
a11y.focusFirst($elementToFocus);
}
const index = item.get('_index');
this.model.set('_activeItemIndex', index);

onItemsVisitedChange(item, _isVisited) {
if (!_isVisited) return;
this.$(`[data-index="${item.get('_index')}"]`).addClass('is-visited');
this.manageBackNextStates(index);
this.setStage(item);
}

calculateMode() {
const mode = device.screenSize === 'large' ? MODE.LARGE : MODE.SMALL;
this.model.set('_mode', mode);
this.model.set('_isLargeMode', mode === MODE.LARGE);
}

renderMode() {
this.calculateMode();

const isLargeMode = this.isLargeMode();
const isTextBelowImage = this.isTextBelowImage();
this.$el
.toggleClass('mode-large', isLargeMode)
.toggleClass('mode-small', !isLargeMode)
.toggleClass('items-are-full-width', isTextBelowImage);
this.model.set('_isTextBelowImageResolved', isTextBelowImage);
}

isLargeMode() {
Expand All @@ -107,10 +81,6 @@ class NarrativeView extends ComponentView {
this.setupNarrative();

this.$('.narrative__slider').imageready(this.setReadyStatus.bind(this));

if (Adapt.config.get('_disableAnimation')) {
this.$el.addClass('disable-animation');
}
}

setupNarrative() {
Expand All @@ -133,7 +103,7 @@ class NarrativeView extends ComponentView {
this.replaceInstructions();
}
this.setupEventListeners();
this._isInitial = false;
this.model.set('_isInitial', false);
}

calculateWidths() {
Expand All @@ -148,7 +118,7 @@ class NarrativeView extends ComponentView {
const previousMode = this.model.get('_mode');
this.renderMode();
if (previousMode !== this.model.get('_mode')) this.replaceInstructions();
this.evaluateNavigation();
this.setupBackNextLabels();
const activeItem = this.model.getActiveItem();
if (activeItem) this.setStage(activeItem);
}
Expand Down Expand Up @@ -208,89 +178,93 @@ class NarrativeView extends ComponentView {
if (Adapt.config.get('_defaultDirection') === 'ltr') {
offset *= -1;
}
const cssValue = `translateX(${offset}%)`;
const $sliderElm = this.$('.narrative__slider');
const $straplineHeaderElm = this.$('.narrative__strapline-header-inner');

$sliderElm.css('transform', cssValue);
$straplineHeaderElm.css('transform', cssValue);
this.model.set('_translateXOffset', offset);
}

setStage(item) {
const index = item.get('_index');
const indexSelector = `[data-index="${index}"]`;

if (this.isLargeMode()) {
// Set the visited attribute for large screen devices
item.toggleVisited(true);
}

this.$('.narrative__progress').removeClass('is-selected').filter(indexSelector).addClass('is-selected');

const $slideGraphics = this.$('.narrative__slider-image-container');
a11y.toggleAccessibleEnabled($slideGraphics, false);
a11y.toggleAccessibleEnabled($slideGraphics.filter(indexSelector), true);

const $narrativeItems = this.$('.narrative__content-item');
$narrativeItems.addClass('u-visibility-hidden u-display-none');
a11y.toggleAccessible($narrativeItems, false);
a11y.toggleAccessible($narrativeItems.filter(indexSelector).removeClass('u-visibility-hidden u-display-none'), true);

const $narrativeStraplineButtons = this.$('.narrative__strapline-btn');
a11y.toggleAccessibleEnabled($narrativeStraplineButtons, false);
a11y.toggleAccessibleEnabled($narrativeStraplineButtons.filter(indexSelector), true);

this.evaluateNavigation();
this.setupBackNextLabels();
this.evaluateCompletion();
this.shouldShowInstructionError();
this.moveSliderToIndex(index);
}

evaluateNavigation() {
const active = this.model.getActiveItem();
if (!active) return;
/**
* Controls whether the back and next buttons should be enabled
*
* @param {Number} [index] Item's index value. Defaults to the currently active item.
*/
manageBackNextStates(index = this.model.getActiveItem().get('_index')) {
const totalItems = this.model.getChildren().length;
const canCycleThroughPagination = this.model.get('_canCycleThroughPagination');

const index = active.get('_index');
const itemCount = this.model.getChildren().length;
const shouldEnableBack = index > 0 || canCycleThroughPagination;
const shouldEnableNext = index < totalItems - 1 || canCycleThroughPagination;

const isAtStart = index === 0;
const isAtEnd = index === itemCount - 1;
this.model.set('shouldEnableBack', shouldEnableBack);
this.model.set('shouldEnableNext', shouldEnableNext);
}

const $left = this.$('.narrative__controls-left');
const $right = this.$('.narrative__controls-right');
/**
* Construct back and next aria labels
*
* @param {Number} [index] Item's index value.
*/
setupBackNextLabels(index = this.model.getActiveItem().get('_index')) {
const totalItems = this.model.getChildren().length;
const canCycleThroughPagination = this.model.get('_canCycleThroughPagination');

const globals = Adapt.course.get('_globals');
const isAtStart = index === 0;
const isAtEnd = index === totalItems - 1;

const ariaLabelsGlobals = globals._accessibility._ariaLabels;
const globals = Adapt.course.get('_globals');
const narrativeGlobals = globals._components._narrative;

const ariaLabelPrevious = narrativeGlobals.previous || ariaLabelsGlobals.previous;
const ariaLabelNext = narrativeGlobals.next || ariaLabelsGlobals.next;

const prevTitle = isAtStart ? '' : this.model.getItem(index - 1).get('title');
const nextTitle = isAtEnd ? '' : this.model.getItem(index + 1).get('title');

a11y.toggleEnabled($left, !isAtStart);
a11y.toggleEnabled($right, !isAtEnd);
let prevTitle = isAtStart ? '' : this.model.getItem(index - 1).get('title');
let nextTitle = isAtEnd ? '' : this.model.getItem(index + 1).get('title');

let backItem = isAtStart ? null : index;
let nextItem = isAtEnd ? null : index + 2;

if (canCycleThroughPagination) {
if (isAtStart) {
prevTitle = this.model.getItem(totalItems - 1).get('title');
backItem = totalItems;
}
if (isAtEnd) {
nextTitle = this.model.getItem(0).get('title');
nextItem = 1;
}
}

$left.attr('aria-label', Handlebars.helpers.compile_a11y_normalize(ariaLabelPrevious, {
const backLabel = compile(narrativeGlobals.previous, {
_globals: globals,
title: prevTitle,
itemNumber: backItem,
totalItems
});

const nextLabel = compile(narrativeGlobals.next, {
_globals: globals,
itemNumber: isAtStart ? null : index,
totalItems: itemCount
}));
$right.attr('aria-label', Handlebars.helpers.compile_a11y_normalize(ariaLabelNext, {
title: nextTitle,
_globals: globals,
itemNumber: isAtEnd ? null : index + 2,
totalItems: itemCount
}));
itemNumber: nextItem,
totalItems
});

this.model.set('backLabel', backLabel);
this.model.set('nextLabel', nextLabel);
}

evaluateCompletion() {
if (this.model.areAllItemsCompleted()) {
this.trigger('allItems');
this.$('.narrative__instruction-inner').removeClass('instruction-error');
this.$('.narrative__instruction').removeClass('has-error');
}
}

Expand All @@ -307,9 +281,10 @@ class NarrativeView extends ComponentView {
});
}

onNavigationClicked(event) {
const $btn = $(event.currentTarget);
onNavigationClicked(e) {
const $btn = $(e.currentTarget);
let index = this.model.getActiveItem().get('_index');

$btn.data('direction') === 'right' ? index++ : index--;
this.model.setActiveItem(index);
}
Expand All @@ -324,19 +299,14 @@ class NarrativeView extends ComponentView {
this.model.setActiveItem(--index);
}

onProgressClicked(event) {
const index = $(event.target).data('index');
this.model.setActiveItem(index);
}

/**
* In mobile view, highlight instruction if user navigates to another
* item before completing, in case the strapline is missed
*/
shouldShowInstructionError() {
const prevItemIndex = this.model.getActiveItem().get('_index') - 1;
if (prevItemIndex < 0 || this.model.getItem(prevItemIndex).get('_isVisited')) return;
this.$('.narrative__instruction-inner').addClass('instruction-error');
this.$('.narrative__instruction').addClass('has-error');
}

setupEventListeners() {
Expand All @@ -346,6 +316,6 @@ class NarrativeView extends ComponentView {
}
}

NarrativeView.template = 'narrative';
NarrativeView.template = 'narrative.jsx';

export default NarrativeView;
26 changes: 18 additions & 8 deletions less/narrative.less
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
.narrative {
&:not(.items-are-full-width) &__content {
&__instruction.has-error {
display: flex;
color: @validation-error;

.icon {
.icon-exclamation;
}
}

&__inner:not(.items-are-full-width) &__content {
display: none;

@media (min-width: @device-width-medium) {
Expand All @@ -15,6 +24,11 @@
}
}

&__content-item:not(.is-active) {
.u-display-none;
.u-visibility-hidden;
}

// Strapline
// Clickable element on mobile, opens notify
// --------------------------------------------------
Expand Down Expand Up @@ -92,11 +106,6 @@

&__slider-image {
.u-no-select;
-webkit-user-drag: none;
-khtml-user-drag: none;
-moz-user-drag: none;
-o-user-drag: none;
user-drag: none;
}

// Button controls
Expand Down Expand Up @@ -155,7 +164,6 @@
height: @icon-size / 2;
width: @icon-size / 2;
margin: 0.25rem;
cursor: pointer;
background-color: @background;
}

Expand All @@ -169,7 +177,9 @@
html:not(.disable-animation) & {
&__slider,
&__strapline-header-inner {
.transition(transform 400ms ease-in-out);
@media (prefers-reduced-motion: no-preference) {
.transition(transform 400ms ease-in-out);
}
}
}

Expand Down
Loading