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

Add tabs component #776

Merged
merged 6 commits into from
Jun 20, 2018
Merged

Add tabs component #776

merged 6 commits into from
Jun 20, 2018

Conversation

alex-ju
Copy link
Contributor

@alex-ju alex-ju commented Jun 8, 2018

Review page

https://govuk-frontend-review-pr-776.herokuapp.com/components/tabs

Contribution log

alphagov/govuk-design-system-backlog#100 (comment)

Browser and AT testing

Chrome 67 chrome67
Safari 11 safari11
Firefox 60 with inverted colours ff60
IE11 ie11
IE10 ie10
IE9 ie9
IE8 ie8
NVDA with Firefox

Tab control. 'Past day' tab, selected, 1 of 4.

JAWS with IE11

'Past day' tab, selected, 1 of 4.

VoiceOver on Safari

'Past day', selected, tab, 1 of 4.

Progress

  • Update to follow the SCSS styleguide and uses govuk-frontend varibles and mixins
  • Define example in the yaml schema
  • Add Nunjucks macro and unit tests for the markup
  • Update to follow the JavaScript styleguide
  • Add polyfills for less-capable browser support or has a good fallback
  • Integrations tests for the JavaScript
  • Rebase and adjust based on recent govuk-frontend changes (i.e. mixin names and variables)
  • Update example
  • A new round of AT testing

Trello card

var id = $panel.id
$panel.id = ''
this.changingHash = true
window.location.hash = this.getHref($tab).slice(1)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we sure we want to be adding the history stack?

User needs:

  1. As a user I want to be able to copy and paste the URL and have tabs show in the same way when I go to this URL
  2. As a user I want to be able to go back to the previous page, despite how many tabs I've interacted with.

With this in mind, I recommend we consider using replaceState

if (typeof window.matchMedia === 'function') {
this.setupResponsiveChecks()
} else {
this.setup()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previously, this feature detect was around the whole component which meant that on small screens no tabs will be setup.

Am I right in thinking that in browsers that don't support matchMedia, they'll get tabs regardless of screen size?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that the mobile browsers that we support all have good support for matchMedia maybe this isn't too much of a worry.

With the assumption that most of the time IE9 and below are used by users on desktop machines with wide screens?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it's probably okay. Just to note two examples where it's not as robust:

An IE9 user who opens up two windows side by side will get tabs in a small-ish screen.

Similarly, Blackberry 7, for example, will get tabs on their small screen.

In both cases, they will experience some layout issues such as horizontal scrolling or wrapping of tabs.

@joelanman
Copy link
Contributor

Just to note I think the example needs changing - this example is a pattern that does not have clear labels, and is no longer used by gov.uk

The labels issue was brought up in the working group review: alphagov/govuk-design-system-backlog#100 (comment)

@alex-ju alex-ju force-pushed the add-tabs-component branch 2 times, most recently from 91ca602 to d830559 Compare June 8, 2018 15:45
@timpaul
Copy link
Contributor

timpaul commented Jun 15, 2018

Hey @joelanman - how about this example from the Judicial Case Manager?:

image

(the respondent tab has a similar table in it)

@joelanman
Copy link
Contributor

It's ok, I'm sure in context for these users that the labels are clear, but it would be nice if we could find one where the labels are just clear to most people.

@timpaul
Copy link
Contributor

timpaul commented Jun 18, 2018

@joelanman - here is a fictional but I think plausible alternative:

image

Each tab has a similar table, but the numbers are different.

Incidentally (@adamsilver you might have an opinion on this) - do we think that each tab should have a section heading? I don't think the above example needs them, but they might be necessary for accessibility / non-JS...

@adamsilver
Copy link
Contributor

adamsilver commented Jun 18, 2018

I don't think the Tab component needs to dictate whether each panel should be marked up with a heading or not. If headings are needed, then they'll be added to the HTML. If not, they won't be.

The Tab component itself does provide an accessible label (labelled by the panel's related tab label) so that's non-sighted users sorted.

If there's a scenario whereby we want headings without JS but no (visual) headings with JS, then I'd imagine the heading would be given a class of js-hidden which provides that functionality?

@alex-ju alex-ju force-pushed the add-tabs-component branch 2 times, most recently from a2bdc29 to 720371d Compare June 18, 2018 14:29
@alex-ju alex-ju changed the title [WIP] Add tabs component Add tabs component Jun 18, 2018

## Introduction

Component for conditionally revealing content, using the details HTML element.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure this is right

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good spot, thanks!

],
[
{
text: 'item.id'
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we changed the way we show item properties from item.id to items.{}.id

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated all the item properties and borrowed he explanations for consistency.

@alex-ju alex-ju force-pushed the add-tabs-component branch 3 times, most recently from 69375e7 to d380465 Compare June 19, 2018 10:00
@govuk-design-system-ci govuk-design-system-ci temporarily deployed to govuk-frontend-review-pr-776 June 19, 2018 10:32 Inactive
@govuk-design-system-ci govuk-design-system-ci temporarily deployed to govuk-frontend-review-pr-776 June 19, 2018 13:25 Inactive
@govuk-design-system-ci govuk-design-system-ci temporarily deployed to govuk-frontend-review-pr-776 June 19, 2018 13:27 Inactive
Copy link
Contributor

@36degrees 36degrees left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not had a chance to go through the tests yet.

}

// JavaScript enabled
.js-enabled .govuk-tabs {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To try and keep specificity to a minimum, should this just be .js-enabled?

this.checkMode(this.mql)
}

Tabs.prototype.checkMode = function (mql) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mql is passed in but never used - within the function we use this.mql.

Tabs.prototype.setupResponsiveChecks = function () {
this.mql = window.matchMedia('(min-width: 40.0625em)')
this.mql.addListener(this.checkMode.bind(this))
this.checkMode(this.mql)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Passing this.mql unnecessarily as checkMode never uses it.


// Remove old active panels
this.unhighlightTab($tab)
this.hidePanel($tab)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we change these two calls for hideTab which wraps them both up together?

var $activeTab = this.getTab(window.location.hash)
if ($activeTab) {
this.highlightTab($activeTab)
this.showPanel($activeTab)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Likewise I believe both highlightTab and showPanel can be replaced by showTab?

this.changingHash = false
return
}
var $hashTab = this.getTab(window.location.hash)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can use hash rather than window.location.hash.

Would also consider skipping hasTab and storing and using the result from getTab which would mirror what we do in setup (var $activeTab = this.getTab(window.location.hash))

}
var $hashTab = this.getTab(window.location.hash)
var $currentTab = this.getCurrentTab()
if ($hashTab) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This must be true if we got here? Because we can only reach this code if we didn't return as a result of !this.hasTab.

this.showTab($hashTab)
$hashTab.focus()
} else {
var $firstTab = this.$tabs.firstElementChild
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is dead code that can be removed, but if not it'd be good to reduce the duplication.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not dead code, as when there's not hash it falls back to the first tab. Will try and reduce the logic.

}

Tabs.prototype.hasTab = function (hash) {
return this.$module.querySelector(hash)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason this uses a different selector to getTab?


function Tabs ($module) {
this.$module = $module
this.$tabs = $module.querySelectorAll('.govuk-tabs__tab')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have we tested having multiple tab components on a single page?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, did tested with multiple per page.

@govuk-design-system-ci govuk-design-system-ci temporarily deployed to govuk-frontend-review-pr-776 June 19, 2018 15:09 Inactive
@govuk-design-system-ci govuk-design-system-ci temporarily deployed to govuk-frontend-review-pr-776 June 19, 2018 15:33 Inactive
@govuk-design-system-ci govuk-design-system-ci temporarily deployed to govuk-frontend-review-pr-776 June 19, 2018 15:41 Inactive
@alex-ju
Copy link
Contributor Author

alex-ju commented Jun 19, 2018

Updated addressing all comments.

@36degrees 36degrees added this to the v1.0.0 milestone Jun 20, 2018
@govuk-design-system-ci govuk-design-system-ci temporarily deployed to govuk-frontend-review-pr-776 June 20, 2018 11:38 Inactive
Tabs.prototype.createHistoryEntry = function ($tab) {
var $panel = this.getPanel($tab)
var id = $panel.id
$panel.id = ''
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we have to null and restore the panel's id? If this is behaviour we definitely need, can we add a comment to explain why?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is so the page doesn't jump when a user clicks a tab (which changes the
hash).

@@ -31,6 +32,11 @@ function initAll () {
nodeListForEach($radios, function ($radio) {
new Radios($radio).init()
})

var $tabs = document.querySelectorAll('[data-module="tabs"]')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At the moment the macro template has the data attribute on the container. This means that if I use the macro, it will automatically get instantiated into tabs.

This could become problematic if I want to do something more custom. Like listen to an event fired by the instance (I know we can't do that right now).

Is GOV.UK Frontend continuing to instantiate automatically like this?

Copy link
Contributor Author

@alex-ju alex-ju Jun 20, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your concern is valid, but this is consistent with how the other components are being initialised. Will make a note and look into that for all the components.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, it might be okay, broadly speaking, for say tabs and conditional radio button reveals. But as a general approach it can be overly constraining.

Thanks @alex-ju

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This use case is already accounted for: you can avoid initAll() and call the constructor yourself.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we did work to stop initAll() from executing automatically :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, not initialising or initialising separately a specific component is also an option.
When Adam mentioned 'something custom', I was thinking of a case when you want to initialise but hook different listeners or add custom functionality to a Tabs instance before doing that, which I think is still possible with the current model:

var myTabs = new Tabs($tabs)
myTabs.myCustomFunction = function () { ... }
myTabs.init()

@adamsilver
Copy link
Contributor

adamsilver commented Jun 20, 2018

Will the Tabs constructor be a global? Or will it be assigned as GOVUK.Tabs?

@govuk-design-system-ci govuk-design-system-ci temporarily deployed to govuk-frontend-review-pr-776 June 20, 2018 14:00 Inactive
@alex-ju
Copy link
Contributor Author

alex-ju commented Jun 20, 2018

@adamsilver: the Tabs constructor is attached to a GOVUKFrontend global variable and every tab instance will be initialised separately using the new operator.

For example if we consider $tabs a queried tab element, the JavaScript initialisation will be:
new GOVUKFrontend.Tabs($tabs).init()

Copy link
Contributor

@36degrees 36degrees left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! 👍📑

@alex-ju alex-ju merged commit 1b9ca1c into master Jun 20, 2018
@alex-ju alex-ju deleted the add-tabs-component branch June 20, 2018 15:26
This was referenced Jun 21, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants