Skip to content

Instantly share code, notes, and snippets.

@krivaten
Last active January 2, 2018 21:26
Show Gist options
  • Save krivaten/3dc4ae4c8db94f5352489590c7d8ca56 to your computer and use it in GitHub Desktop.
Save krivaten/3dc4ae4c8db94f5352489590c7d8ca56 to your computer and use it in GitHub Desktop.
New Twiddle
import Ember from 'ember';
import hbs from 'htmlbars-inline-precompile';
const {
Component
} = Ember;
export default Component.extend({
classNames: ['alert-container'],
attributeBindings: [
'role',
'live:aria-live',
'atomic:aria-atomic'
],
layout: hbs`
{{#if message}}
<div class="alert alert-{{type}}">
{{#if title}}
<strong class={{if titleHidden 'sr-only'}}>{{title}}:</strong>
{{/if}}
{{message}}
</div>
{{/if}}
`,
type: 'info',
title: null,
titleHidden: false,
message: null,
role: 'alert',
live: 'polite',
atomic: 'true'
});
import Ember from 'ember';
import UiNum from 'twiddle/components/ui-num';
import { numToWords } from 'twiddle/utils/numbers';
const {
get,
computed
} = Ember;
export default UiNum.extend({
classNames: ['ui-currency'],
number: null,
currencySymbol: "$",
splitAmount: computed('number', function() {
let number = get(this, 'number') || '0';
return parseFloat(number).toFixed(2).split('.');
}),
formattedAmount: computed('number', function() {
let splitAmount = get(this, 'splitAmount');
let currencySymbol = get(this, 'currencySymbol');
splitAmount[0] = splitAmount[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
return `${currencySymbol}${splitAmount.join('.')}`;
}),
label: computed('number', function() {
let splitAmount = get(this, 'splitAmount');
let result = [];
splitAmount.forEach((item) => {
result.push((item && item !== '00') ? numToWords(item) : null);
});
let [ dollars, cents ] = result;
if (dollars && cents) {
return `${dollars} dollars and ${cents} cents`;
} else {
return `${dollars} dollars`;
}
})
});
import Ember from 'ember';
const {
Component,
get,
computed,
} = Ember;
const UiIcon = Component.extend({
tagName: 'span',
classNameBindings: [
'prefix',
'iconClass'
],
attributeBindings: [
'label:aria-label',
'_ariaHidden:aria-hidden'
],
label: null,
prefix: 'fa',
icon: null,
_ariaHidden: computed('label', function() {
return get(this, 'label') ? undefined : 'true';
}),
iconClass: computed('icon', function() {
const prefix = get(this, 'prefix');
const icon = get(this, 'icon');
if (!icon) {
return;
}
return (icon.indexOf(`${prefix}-`) > -1) ? icon : `${prefix}-${icon}`;
})
});
UiIcon.reopenClass({
positionalParams: ['icon']
});
export default UiIcon;
import Ember from 'ember';
import hbs from 'htmlbars-inline-precompile';
const {
Component,
computed,
assert,
isPresent,
get,
guidFor
} = Ember;
const LABEL_MSG = 'You must provide a "label" attribute for all uses of "{{ui-input}}" for impaired users. If you want to hide the label visually, you may also provide the attribute labelHidden=true.';
export default Component.extend({
classNames: ['form-group'],
layout: hbs`
<label for="{{inputId}}" class="{{if labelHidden 'sr-only'}}">
{{label}}
{{#if required}}
<sup class="text-danger">*</sup>
{{/if}}
</label>
{{#if hasBlock}}
{{yield this}}
{{else}}
{{input
id=inputId
ariaDescribedBy=(if description descriptionId)
type=type
value=value
placeholder=placeholder
disabled=disabled
required=required
class="form-control"}}
{{/if}}
{{#if description}}
<p id="{{descriptionId}}" class="text-muted {{if descriptionHidden 'sr-only'}}">
{{description}}
</p>
{{/if}}
`,
id: null,
type: 'text',
value: null,
placeholder: null,
disabled: null,
required: null,
labelHidden: null,
label: computed({
set(key, value) {
assert(LABEL_MSG, isPresent(value));
return value;
}
}),
containerId: computed('id', function() {
return get(this, 'id') || guidFor(this);
}),
inputId: computed('id', function() {
return `${get(this, 'containerId')}-input`;
}),
descriptionId: computed('containerId', function() {
return `${get(this, 'containerId')}-description`;
})
});
import Ember from 'ember';
import hbs from 'htmlbars-inline-precompile';
import { numToWords } from 'twiddle/utils/numbers';
const {
Component,
computed,
get
} = Ember;
const UiNum = Component.extend({
classNames: ['ui-num'],
tagName: 'span',
attributeBindings: [
'label:aria-label'
],
layout: hbs`{{formattedAmount}}`,
number: null,
splitAmount: computed('number', function() {
let number = String(get(this, 'number')) || '0';
return number.split('.');
}),
formattedAmount: computed('splitAmount', function() {
let splitAmount = get(this, 'splitAmount');
splitAmount[0] = splitAmount[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
return splitAmount.join('.');
}),
label: computed('splitAmount', function() {
let splitAmount = get(this, 'splitAmount');
let result = [];
splitAmount.forEach((item) => result.push(numToWords(item)));
return result.join(' point ');
})
});
UiNum.reopenClass({
positionalParams: ['number']
});
export default UiNum;
import Ember from 'ember';
export default Ember.Controller.extend({
queryParams: ['refreshValue'],
refreshValue: null,
actions: {
refreshIt() {
console.log('Triggered refreshIt')
this.send('refreshModel')
}
}
});
import Ember from 'ember';
export function initializeAria() {
Ember.TextSupport.reopen({
attributeBindings: [
'ariaDescribedBy:aria-describedby'
]
});
}
export default {
name: 'aria',
initialize: initializeAria
};
import Ember from 'ember';
import config from './config/environment';
const Router = Ember.Router.extend({
location: 'none',
rootURL: config.rootURL
});
Router.map(function() {
this.route('icons');
this.route('inputs');
this.route('alerts');
this.route('numbers');
this.route('currency');
});
export default Router;
import Ember from 'ember';
export default Ember.Route.extend({
queryParams: {
refreshValue: {
refreshModel: true
}
},
actions: {
refreshModel() {
console.log('Triggered refreshModel');
this.refresh();
}
}
});
@import "https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css";
@import "https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css";
.focusing-outlet:focus {
background-color: Azure;
}
.fa-crap:before {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
content: 'You should not hear this.';
color: black;
}
<h1>Alerts</h1>
<p>There will be times when you need to let a user know when something has changed in the DOM. Typically, we build alert systems to do this. However, screen readers won't necessarily pick up on those notifications unless we tell them to. That's where the <code>ui-alert</code> component comes in. It renders a container that will be used for alerts, and will be read back to the user when their content changes.</p>
<h2>Examples</h2>
<h3>Polite</h3>
<p>This first example will be populated when you click the buttons below. By default, the screen reader will wait until it is done reading whatever it is currently presenting, before moving on to the notification. This is because the component is using the <code>aria-live</code> attribute, set to "polite".</p>
<button class="btn btn-primary" onclick={{action (mut messageOne) 'Can a kangaroo jump higher than a house?'}}>Show First Message</button>
<button class="btn btn-secondary" onclick={{action (mut messageOne) 'Of course, a house doesn’t jump at all.'}}>Show Second Message</button>
{{ui-alert title='Joke' message=messageOne}}
<h3>Assertive</h3>
<p>In the event that we need the screen reader to stop immediately and present the updated content back to the user, we can pass in the attribute <code>live</code> with a value of <em>assertive</em>, which will do just that.</p>
<button class="btn btn-primary" onclick={{action (mut messageTwo) 'This is a test of the emergency broadcast system'}}>Show First Message</button>
<button class="btn btn-secondary" onclick={{action (mut messageTwo) 'Broadcasters, in cooperation with the FCC and other authorities have developed this system to keep you informed in the event of an emergency'}}>Show Second Message</button>
{{ui-alert live='assertive' type='danger' title='Emergency Broadcast System' message=messageTwo}}
<div class="container">
<header class="navbar navbar-inverse">
<div class="navbar-header">
<a class="navbar-brand" href="#" aria-label="Accessibility">A11Y</a>
</div>
<nav id="navbar" class="navbar-collapse">
<ul class="nav navbar-nav">
<li>
{{#link-to 'index'}}
{{ui-icon 'home' class="fa-fw"}}
Home
{{/link-to}}
</li>
<li>
{{#link-to 'icons'}}
{{ui-icon 'rocket' class="fa-fw"}}
Icons
{{/link-to}}
</li>
<li>
{{#link-to 'inputs'}}
{{ui-icon 'archive' class="fa-fw"}}
Inputs
{{/link-to}}
</li>
<li>
{{#link-to 'alerts'}}
{{ui-icon 'bell' class="fa-fw"}}
Alerts
{{/link-to}}
</li>
<li>
{{#link-to 'numbers'}}
{{ui-icon 'hashtag' class="fa-fw"}}
Numbers
{{/link-to}}
</li>
<li>
{{#link-to 'currency'}}
{{ui-icon 'usd' class="fa-fw"}}
Currency
{{/link-to}}
</li>
</ul>
</nav>
</header>
</div>
<main class="container">
<div class="alert alert-warning">
{{ui-icon 'warning-sign' label='Warning:'}}
Be sure to turn on your screen reader (⌘+F5 on Mac)!
</div>
{{focusing-outlet}}
</main>
<h1>Currency</h1>
<p>Much like we mentioned with our <code>ui-num</code> component, numbers are hard to get presented well with screen readers. In that same vein comes the challenge of reading out currencies.</p>
<p>If you see the value {{ui-currency '53.25'}}, you will read that in your head as, "fifty three dollars and twenty five cents". However, a screen reader will read it as "dollars five three two five" (Which is not even correct). Again we are faced with that same problem as with numbers, but requiring a little more finesse.</p>
<p>So that is the problem we tackled with the <code>ui-currency</code> component. It will read currencies back to you as we tend to read them in our heads.</p>
<h2>Examples</h2>
<p>Without using this component a screen reader will present the following dollar amount as <em>$243.47</em>. But when we drop in our component, that same amount is presented as <em>{{ui-currency '243.47'}}</em>. If your screen reader is turned off you wouldn't notice a difference. But with a screen reader you will hear that one is completely incorrect, while the other just makes sense.</p>
<p>For the sake of example, some more dollar amounts are {{ui-currency '12'}}, {{ui-currency '0.25'}}, and {{ui-currency '831241.87'}}.</p>
<h1>Icons</h1>
<p>This page is made to feature the <code>ui-icon</code> component. While Font Awesome does a pretty good job of using unicode characters that screen readers won't read, I've added one manual example in the CSS of this Twiddle to show why this component is important.</p>
<h2>Example Uses</h2>
<p><i class="fa fa-crap"></i> But you did. Why? Because CSS is adding content to the DOM, via the <code>before</code> attribute, that a screen reader can read, so it does. If you want assistive technology to ignore an icon, you need the <code>aria-hidden</code> attribute <span aria-hidden="true">(Example: {{ui-icon 'hashtag'}} or {{ui-icon 'bug'}})</span>. Alternatively, if you want a screen reader to state what that icon is for, then use the <code>aria-label</code> attribute to provide a description for the icon (Example: {{ui-icon 'trash' label='Delete'}} or {{ui-icon 'home' label='Navigate to home page'}}</p>
{{input value=refreshValue}}
<button onclick={{action 'refreshIt'}}>Test It</button>
<h1>Welcome</h1>
<p>This is a Twiddle for a blog post series written by Kris Van Houten about writing accessible Ember apps. If you want to read that series, head over to krivaten.com (Sorry I can't link to external sites in Twiddles).</p>
<p>If you meant to be here, well then, that's awesome. Go ahead and turn on your screen reader and start tabbing / clicking around to see how things sound / work.</p>
<p>Cheers!</p>
<h1>Inputs</h1>
<p>Forms are huge buckets of trouble when it comes to accessibility. That's why I want to demo this <code>ui-input</code> component. A nice feature in this component is that it will yell at us if we try to use it without providing a label. This ensures that other developers who may not be informed on accessibility don't produce inaccessible code.</p>
<h2>Examples</h2>
<form>
<p>When you focus on the following input, a screen reader will give you no indication as to what the field is for:</p>
<div class="form-group">
<label>Anonymous Label</label>
{{input value=anonymous class="form-control"}}
</div>
<p>However, when labels correctly reference their inputs, screen readers can announce the labels of the field when they are focused on:</p>
{{ui-input value=name label='Name'}}
{{ui-input type='email' value=email label='Email'}}
<p>By passing in a <code>yield</code> block, we are able to even use other input types as well:</p>
{{#ui-input label='Message' labelHidden=true description='What would you like to say?' as |component|}}
{{textarea id=component.inputId class='form-control' value=message}}
{{/ui-input}}
<button class="btn btn-primary">Submit</button>
</form>
<h1>Numbers</h1>
<p>When it comes to numbers, screen readers don't do all that well. If you have a number like {{ui-num '4235.50'}}, most screen readers will read that as "4235.50". But the problem is, that isn't how we think of numbers greater than nine. We think of the value as a whole, so when a screen reader suddenly starts reading single digits back to a user that is meant to be a large, multi-digit number, it can be frustrating and confusing.</p>
<p>So in an effort to help those using screen readers, we are going to showcase a nice component called <code>ui-num</code>, that will wrap a number in a span with an aria-label that writes out the number as words.</p>
<h2>Examples</h2>
<p>Without using this component a screen reader will present the following number as <em>397,349,573</em>. But when we drop in our component, that same number is presented as <em>{{ui-num 397349573}}</em>. If your screen reader is turned off you wouldn't notice a difference. But with a screen reader you will hear that one is painful to listen to, while the other is rather pleasant.</p>
<p>Some more examples are the numbers {{ui-num '42.35'}}, {{ui-num 1984}}, and {{ui-num '8675309103984.2'}}.</p>
import Ember from 'ember';
export default function destroyApp(application) {
Ember.run(application, 'destroy');
}
import Resolver from '../../resolver';
import config from '../../config/environment';
const resolver = Resolver.create();
resolver.namespace = {
modulePrefix: config.modulePrefix,
podModulePrefix: config.podModulePrefix
};
export default resolver;
import Ember from 'ember';
import Application from '../../app';
import config from '../../config/environment';
const { run } = Ember;
const assign = Ember.assign || Ember.merge;
export default function startApp(attrs) {
let application;
let attributes = assign({rootElement: "#test-root"}, config.APP);
attributes = assign(attributes, attrs); // use defaults, but you can override;
run(() => {
application = Application.create(attributes);
application.setupForTesting();
application.injectTestHelpers();
});
return application;
}
import resolver from './helpers/resolver';
import {
setResolver
} from 'ember-qunit';
setResolver(resolver);
{
"version": "0.11.1",
"EmberENV": {
"FEATURES": {}
},
"options": {
"use_pods": false,
"enable-testing": false
},
"dependencies": {
"jquery": "https://cdnjs.cloudflare.com/ajax/libs/jquery/1.11.3/jquery.js",
"ember": "2.11.0",
"ember-data": "2.11.0",
"ember-template-compiler": "2.11.0",
"ember-testing": "2.11.0"
},
"addons": {
"ember-a11y": "0.1.15",
"ember-a11y-testing": "0.3.0"
}
}
const TEN = 10;
const ONE_HUNDRED = 100;
const ONE_THOUSAND = 1000;
const ONE_MILLION = 1000000;
const ONE_BILLION = 1000000000;
const ONE_TRILLION = 1000000000000;
const ONE_QUADRILLION = 1000000000000000;
const MAX = 9007199254740992;
const LESS_THAN_TWENTY = [
'zero',
'one',
'two',
'three',
'four',
'five',
'six',
'seven',
'eight',
'nine',
'ten',
'eleven',
'twelve',
'thirteen',
'fourteen',
'fifteen',
'sixteen',
'seventeen',
'eighteen',
'nineteen'
];
const TENTHS_LESS_THAN_HUNDRED = [
'zero',
'ten',
'twenty',
'thirty',
'forty',
'fifty',
'sixty',
'seventy',
'eighty',
'ninety'
];
export function numToWords(number) {
let num = parseInt(number, 10);
if (!isFinite(num)) {
throw new TypeError('Not a finite number: ' + number + ' (' + typeof number + ')');
}
return generateWords(num);
}
function isFinite(value) {
return !(typeof value !== 'number' || value !== value || value === Infinity || value === -Infinity);
}
function generateWords(number) {
let remainder;
let word;
let words = arguments[1];
// We’re done
if (number === 0) {
return !words ? 'zero' : words.join(' ').replace(/,$/, '');
}
// First run
if (!words) {
words = [];
}
// If negative, prepend “minus”
if (number < 0) {
words.push('minus');
number = Math.abs(number);
}
if (number < 20) {
remainder = 0;
word = LESS_THAN_TWENTY[number];
} else if (number < ONE_HUNDRED) {
remainder = number % TEN;
word = TENTHS_LESS_THAN_HUNDRED[Math.floor(number / TEN)];
// In case of remainder, we need to handle it here to be able to add the “-”
if (remainder) {
word += '-' + LESS_THAN_TWENTY[remainder];
remainder = 0;
}
} else if (number < ONE_THOUSAND) {
remainder = number % ONE_HUNDRED;
word = generateWords(Math.floor(number / ONE_HUNDRED)) + ' hundred';
} else if (number < ONE_MILLION) {
remainder = number % ONE_THOUSAND;
word = generateWords(Math.floor(number / ONE_THOUSAND)) + ' thousand';
} else if (number < ONE_BILLION) {
remainder = number % ONE_MILLION;
word = generateWords(Math.floor(number / ONE_MILLION)) + ' million';
} else if (number < ONE_TRILLION) {
remainder = number % ONE_BILLION;
word = generateWords(Math.floor(number / ONE_BILLION)) + ' billion';
} else if (number < ONE_QUADRILLION) {
remainder = number % ONE_TRILLION;
word = generateWords(Math.floor(number / ONE_TRILLION)) + ' trillion';
} else if (number <= MAX) {
remainder = number % ONE_QUADRILLION;
word = generateWords(Math.floor(number / ONE_QUADRILLION)) +
' quadrillion';
}
words.push(word);
return generateWords(remainder, words);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment