Yoast / yoast-components

Accessible React components by Yoast
GNU General Public License v3.0
21 stars 6 forks source link

Initial specification #34

Closed omarreiss closed 8 years ago

omarreiss commented 8 years ago

The onboarding wizard is a generic library that can be used to dynamically generate an installation wizard. The wizard and all of its underlying components should be built with React in ES2015, using Babel to transpile the code to ES5. For consistency we will use browserify to manage JS modules.

var MailChimpSignUp = require("./custom_components/MailChimpSignup")
var PublishingEntity = require("./custom_components/PublishingEntity")
var PostTypeVisibility = require("./custom_components/PostTypeVisibility")
var ConnectGoogleSearchConsole = require("./custom_components/ConnectGoogleSearchConsole")

var props = {
  "endpoint": "http://local.wordpress.dev/wp-admin/yoast/onboarding?wp_nonce=nonce",
  "customComponents": [
    MailChimpSignUp,
    PublishingEntity,
    PostTypeVisibility
    ConnectGoogleSearchConsole
  ]
  "fields": {
    "upsellConfigurationService": {
      "component": "HTML",
      "properties": {
        "html": "You can now have Yoast configure Yoast SEO for you."
      }
    },
    "upsellSiteReview": {
      "component": "HTML",
      "properties": {
        "html": "Get more visitors! Our SEO website review will tell you what to improve!"
      }
    },
    "successMessage": {
      "component": "HTML",
      "properties": {
        "html": "Good Job! You've finished setting up Yoast SEO. Thereby you've covered the technical part of your site's SEO. Now it's time to focus on optimizing your content for onpage SEO. You can use our content analysis for that: <br>{gif_showing_content_analysis}"
      }
    },
    "mailchimpSignup": {
      "component": "MailchimpSignup",
      "properties": {
        "label": "If you would like us to keep you up-to-date regarding Yoast SEO and other plugins by Yoast, subscribe to our newsletter:",
        "mailchimpActionUrl": "{http://yoast.us1.list-manage1.com/subscribe/post?u=ffa93edfe21752c921f860358&amp;id=972f1c9122}",
        "currentUserEmail": "{current_user_email:String}"
      },
      "data": ""
    },
    "environment": {
      "component": "Choice",
      "properties": {
        "label": "Please specify the environment {site_url} is running in.",
        "choices": {
          "production": {
            "label": "Production - live site."
          },
          "staging": {
            "label": "Staging - copy of live site used for testing purposes only."
          },
          "development": {
            "label": "Development - locally running site used for development purposes."
          }
        },
      },
      "data": "",
      "default": "production"
    },
    "siteType": {
      "component": "Choice",
      "properties": {
        "label": "What type of site is {site_url}?",
        "choices": {
          "blog": {
            "label": "Blog"
          },
          "shop": {
            "label": "Shop"
          },
          "news": {
            "label": "News site"
          },
          "smallBusiness": {
            "label": "Small business site"
          },
          "corporateOther": {
            "label": "Other corporate site"
          },
          "personalOther": {
            "label": "Other personal site"
          }
        }
      },
      "data": ""
    },
    "publishingEntity": {
      "component": "PublishingEntity",
      "data": {
        "publishingEntityType": "{publishing_entity_type}",
        "publishingEntityPersonName": "{publishing_entity_person_name}",
        "publishingEntityCompanyName": "{publishing_entity_company_name}",
        "publishingEntityCompanyLogo": "{publishing_entity_company_logo}"
      },
      "defaults": {
        "publishingEntityType": "",
        "publishingEntityPersonName": "{currentuser.name}",
        "publishingEntityCompanyName": "",
        "publishingEntityCompanyLogo": ""
      }
    },
    "profileUrlFacebook": {
      "component": "Input",
      "properties": {
        "label": "Facebook page url",
        "pattern": "^https:\/\/www\.facebook\.com\/([^/]+)\/$"
      },
      "data": "{profile_url_facebook}"
    },
    "profileUrlTwitter": {
      "component": "Input",
      "properties": {
        "label": "Twitter url",
        "pattern": "^https:\/\/twitter\.com\/([^/]+)$"
      },
      "data": "{profile_url_twitter}"
    },
    "profileUrlInstagram": {
      "component": "Input",
      "properties": {
        "label": "Instagram url",
        "pattern": "^https:\/\/www\.instagram\.com\/([^/]+)\/$"
      },
      "data": "{profile_url_instagram}"
    },
    "profileUrlLinkedIn": {
      "component": "Input",
      "properties": {
        "label": "LinkedIn url",
        "pattern": "^https:\/\/www\.linkedin\.com\/in\/([^/]+)$"
      },
      "data": "{profile_url_linkedin}"
    },
    "profileUrlMySpace": {
      "component": "Input",
      "properties": {
        "label": "MySpace url",
        "pattern": "^https:\/\/myspace\.com\/([^/]+)\/$"
      },
      "data": "{profile_url_myspace}"
    },
    "profileUrlPinterest": {
      "component": "Input",
      "properties": {
        "label": "Pinterest url",
        "pattern": "^https:\/\/www\.pinterest\.com\/([^/]+)\/$"
      },
      "data": "{profile_url_pinterest}"
    },
    "profileUrlYouTube": {
      "component": "Input",
      "properties": {
        "label": "YouTube url",
        "pattern": "^https:\/\/www\.youtube\.com\/([^/]+)$"
      },
      "data": "{profile_url_youtube}"
    },
    "profileUrlGooglePlus": {
      "component": "Input",
      "properties": {
        "label": "Google+ URL",
        "pattern": "^https:\/\/plus\.google\.com\/([^/]+)$"
      },
      "data": "{profile_url_google_plus}"
    },
    "multipleAuthors": {
      "component": "Choice",
      "properties": {
        "label": "Does your site have multiple authors?",
        "choices": {
          "yes": {
            "label": "Yes"
          },
          "no": {
            "label": "No"
          }
        }
      },
      "data": ""
    },
    "tagLine": {
      "component": "Input",
      "properties": {
        "label": "You still have the default WordPress tagline, even an empty one is probably better. Please clear it or replace it with something unique.",
      },
      "data": "{wp_tagline}",
    },
    "postTypeVisibility": {
      "component": "PostTypeVisibility",
      "properties": {
        "label": "Please specify if which of the following public post types you would like Google to see",
        "postTypes": {
          "locations": "Locations",
          "products": "Products"
        }
      },
      "data": {
        "locations": "{:bool}",
        "products": "{:bool}"
      }
    },
    "connectGoogleSearchConsole": {
      "component": "ConnectGoogleSearchConsole",
      "data": {
        "token": "{gsc_token}",
        "profile": "{gsc_profile}"
      }
    },
    "siteName": {
      "component": "Input",
      "properties": {
        "label": "Sitename"
      },
      "data": "{sitename}"
    },
    "separator": {
      "component": "Choice",
      "properties": {
        "label": "Separator",
        "choices": {
          "dash": {
            "label": "&dash;",
            "screenReaderText": "Dash"
          },
          "ndash": {
            "label": "&ndash;",
            "screenReaderText": "En dash"
          },
          "mdash": {
            "label": "&mdash;",
            "screenReaderText": "Em dash"
          },
          "middot": {
            "label": "&middot;",
            "screenReaderText": "Middle dot"
          },
          "bull": {
            "label": "&bull;",
            "screenReaderText": "Bullet"
          },
          "asterisk": {
            "label": "*",
            "screenReaderText": "Asterisk"
          },
          "lowast": {
            "label": "&#8270;",
            "screenReaderText": "Low asterisk"
          },
          "pipe": {
            "label": "|",
            "screenReaderText": "Vertical bar"
          },
          "tilde": {
            "label": "~",
            "screenReaderText": "Small tilde"
          },
          "laquo": {
            "label": "&laquo;",
            "screenReaderText": "Left angle quotation mark"
          },
          "raquo": {
            "label": "&raquo;",
            "screenReaderText": "Right angle quotation mark"
          },
          "lt": {
            "label": "<",
            "screenReaderText": "Less than sign"
          },
          "gt": {
            "label": ">",
            "screenReaderText": "Greater than sign"
          }
        }
      },
      "data": "{separator}"
    },
  },
  "steps": {
    "intro": {
      "title": "Intro",
      "fields": ["upsellConfigurationService", "mailchimpSignup"]
    },
    "environment": {
      "title": "Environment",
      "fields": ["environment"]
    },
    "siteType": {
      "title": "Site type",
      "fields": ["siteType"]
    },
    "publishingEntity": {
      "title": "Company or person",
      "fields" : ["publishingEntity"]
    },
    "profileUrls": {
      "title": "Social profiles",
      "fields" : [
        "profileUrlFacebook",
        "profileUrlTwitter",
        "profileUrlInstagram",
        "profileUrlLinkedIn",
        "profileUrlMySpace",
        "profileUrlPinterest",
        "profileUrlYouTube",
        "profileUrlGooglePlus"
      ]
    },
    "postTypeVisibility": {
      "title": "Post type visibility",
      "fields": ["postTypeVisibility"]
    },
    "multipleAuthors": {
      "title": "Multiple authors",
      "fields": ["multipleAuthors"]
    },
    "connectGoogleSearchConsole": {
      "title": "Google Search Console",
      "fields": ["connectGoogleSearchConsole"]
    },
    "titleTemplate": {
      "title": "Title settings",
      "fields": ["siteName","separator"]
    },
    "tagLine": {
      "title": "Tagline",
      "fields": ["tagLine"]
    },
    "success": {
      "title": "Success!",
      "fields": ["successMessage", "upsellSiteReview", "mailchimpSignup"]
    }
  }
}

var wizard = new Wizard(props)
omarreiss commented 8 years ago

Discussion from previous repo:

@omarreiss:

When it comes to accessibility, the following requirements apply:

  • The wizard should be keyboard accessible.
  • step titles should be h1 headers.
  • All labels should be clickable and linked to their corresponding input fields.
  • All inputs should be focusable.

To be considered:

  • We might want to use A11ySpeak to communicate the navigational flow to screen readers.
    When we...
    • ...append a loader to the button that was clicked.
    • We could tell the screenreader we are saving the current step and loading the next one.
    • ...remove the loader and render the next (or previous) step after successfully saving.
    • We should tell the screenreader the next step is loaded.
    • ...remove the loader and trigger an alert after failing to save.
    • The screenreader should pick up the alert and we don't need to do anything else. It goes without saying that we need to make sure the text of the alert is descriptive enough.

@afercia please advise.

@afercia:

@omarreiss: first things that come into my mind:

  • React: I know very few about React. Learned a few things and read some documentation while working on the KB search. My concern is about the continuous re-rendering of components. As far as I know, each time setState() is called, the component gets re-rendered with a few exceptions (e.g when setState() is used within some React specific functions). Re-rendering a component basically means removing from the DOM a chunk (often a big chunk) of HTML and then re-injecting it with a potential, very likely, focus loss. Especially with some browsers, a focus loss implies a loss of context, forcing users to start tabbing again from the document root, where focus fallbacks. Wondering if this would need some way to manage the focus e.g. when pressing "Next" or "Previous" where the focus is supposed to land?
  • group of radio buttons: please consider they should always be grouped in a fieldset with a legend, as well as other form elements that may be logically grouped. This is always true for radio buttons since they're grouped by their nature. To evaluate case by case for other form elements.
  • in the instantiation example. I see the "separator choices". Some of the symbols there would need a textual alternative since some of them are not announced correctly by screen readers. We've seen this this in the current separator setting in Yoast SEO. Wondering if it is worth considering an abstract, generic method to have textual alternatives.
  • implementation detail: would this wizard be in a form of "in page content" or displayed in some sort of modal dialog? In the second case it would need additional stuff to make the modal dialog accessible.

@omarreiss:

@afercia

  • Risk of focus loss: I think it should be possible to save the focus in the state and retain it at all times. We should keep this in mind while building.
  • Radio buttons: Thanks for the context!
  • Separator choices: Good point! I will change the spec a little bit to allow for further clarification per radio button. The generic method should be built into Yoast SEO.
  • We want the wizard to be rendered on its own page as a standalone module to minimize risk of conflicting JS running on the same page.

@omarreiss:

@afercia Every choice can now also have a screenreaderText. Groundwork for the separator choice looks like this:

"choices": {
  "dash": {
    "label": "&dash;",
    "screenReaderText": "Dash"
  },
  "ndash": {
    "label": "&ndash;",
    "screenReaderText": "En dash"
  },
  "mdash": {
    "label": "&mdash;",
    "screenReaderText": "Em dash"
  },
  "middot": {
    "label": "&middot;",
    "screenReaderText": "Middle dot"
  },
  "bull": {
    "label": "&bull;",
    "screenReaderText": "Bullet"
  },
  "asterisk": {
    "label": "*",
    "screenReaderText": "Asterisk"
  },
  "lowast": {
    "label": "&#8270;",
    "screenReaderText": "Low asterisk"
  },
  "pipe": {
    "label": "|",
    "screenReaderText": "Vertical bar"
  },
  "tilde": {
    "label": "~",
    "screenReaderText": "Small tilde"
  },
  "laquo": {
    "label": "&laquo;",
    "screenReaderText": "Left angle quotation mark"
  },
  "raquo": {
    "label": "&raquo;",
    "screenReaderText": "Right angle quotation mark"
  },
  "lt": {
    "label": "<",
    "screenReaderText": "Less than sign"
  },
  "gt": {
    "label": ">",
    "screenReaderText": "Greater than sign"
  }
}