Skip to main content

Personal Portfolio (Previous)

a previous version of my personal portfolio site built via GatsbyJS

View GitHub Repository

Overview

Tech

Static Site Generator
GatsbyJS
CSS
flexbox and grid layout structured with BEM and ITCSS
JS and Framework
ES6 JS + React

Responsibilities

  • create the design of the site using a combination of Adobe Photoshop and Adobe Illustrator with accessibility in mind
  • write content
  • use Git and GitHub for version control and tracking progress
  • develop React components to be used within GatsbyJS
  • test for browser inconsistencies

Development Notes

In early 2018, I started teaching myself (and falling in love with) React. Since I was in desperate need for an updated personal portfolio, I wanted to find a way to fit React into the equation. While I could create a single page application for my portfolio, it didn't really do me any favors in terms of SEO. That's when I stumbled upon GatsbyJS.

GatsbyJS

Gatbsy is a React-based static site generator, meaning it can create a site with multiple pages using React components. Basically, each individual page within the site is its own component that you can import other components into. Using each component's render method, Gatsby will build the page as a static HTML document.

For example, some components I created to be used on each page of this site include the following:

  • Head — a component used to set some needed metadata on each page
  • Header — a component containing the JSX of the header displayed on each page
  • Footer — a component containing the JSX of the footer displayed on each page

Dynamic Components

The nice thing about Gatsby is that you can still use React to create dynamic elements. An example of a dynamic component created for my portfolio was Video, a custom video player.

While the video player itself is inserted into the static page via its render method, controlling the video is all handled using built-in methods.

/**
 * Method that either plays or pauses the video based on whether the video is currently paused.
 */
toggleVideo(){
	// code to play/pause the video here
}

/**
 * If the component is added directly to a static page, componentDidMount() is called on page load.
 */
componentDidMount(){
	// remove browser-default video controls because JS is enabled
	this.setState({ defaultControls: false });
}

render(){
	// simplified JSX for the sake of brevity
	return (
		<div className={`video${!this.state.controls ? ' video--controls-hidden' : ''}`}>
			{/* custom play/pause button for the video */}
			<button onClick={this.toggleVideo}>play video</button>
			{/* HTML5 video element */}
			<video
				controls={this.state.defaultControls}
				ref={this.videoElement}
				preload="none"
				poster={this.props.poster.src}
			>
				<source src={this.props.mp4} type="video/mp4"/>
			</video>
		</div>
	);
}

In the example above, the video players is dynamically controlled via the click event placed on the play/pause button. If needed, you could even go further and import other components dynamically.

GraphQL

Another bonus to using GatsbyJS is the ability to query your site's data via GraphQL. This means you can dynamically query your pages, markdown files, images, etc. and use that data to output content. When combined with some of GatsbyJS plugins, you have a lot of flexibility.

An example where this came in handy for this site was dynamically generating the main work page. I didn't want to have to add a new piece of work manually; instead, I decided it would be better to query all my work pages, and insert the items automatically.

To accomplish this, I combined a GraphQL query and the gatsby-transformer-javascript-frontmatter plugin. At the top of each individual work page, I placed a snippet like the one below (this is the snippet placed for this page).

export const frontmatter = {
  title: "Personal Portfolio",
  role: "Front-End Development",
  blurb: "my personal portfolio site (the one you're looking at now)",
  thumb: "portfolio_gatsby-thumb.png",
  date: "2019-02-01",
  tech: [
    "React",
    "GatsbyJS",
    "HTML5",
    "responsive",
    "flexbox",
    "grid",
    "SCSS",
    "BEM",
    "ITCSS",
    "GraphQL",
  ],
};

Then on the main work page I create a GraphQL query to grab the frontmatter placed on each individual work page.

const WorkPage = ({ data }) => (
	<Layout>
		<div className="container">
			<h1>Work</h1>
			<PortfolioItems data={data}/>
		</div>
	</Layout>
);

export default WorkPage;

export const portfolioQuery = graphql`
	query PortfolioPages {
		allJavascriptFrontmatter(
			# only grab pages located in the 'word' folder
			filter:{
				fileAbsolutePath:{
				regex:"/work/.+/"
			}
			}
			# sort the pages based on the date placed in the front matter
			sort:{
				fields:[
					frontmatter___date
				]
				order:DESC
			}
		){
			edges{
				node{
					id
					node{
						relativeDirectory,
						name
					}
					# determine what data is available from the query
					frontmatter{
						title,
						blurb,
						role,
						thumb,
						tech
					}
				}
			}
		}
	}

The data from the query can then be used within the page by passing an object containing 'data'. As shown in the example above, I created a PortofolioItems component to handle the data.

Finally, within the PortfolioItems component, I created a loop through each returned asset of the query and passed that data individually to a component that is used to display the invidiual work item.

import React from "react";
import PortfolioItem from "./PortfolioItem";

/**
 * A functional React component used to place a list of PortfolioItem components based on passed data.
 * @param {object} props.data - data returned from a GraphQL query containing JS frontmatter from portfolio pages.
 */
function PortfolioItems(props) {
  // validate the data we received is usable
  if (
    typeof props.data === "object" &&
    typeof props.data.allJavascriptFrontmatter === "object" &&
    props.data.allJavascriptFrontmatter !== null &&
    props.data.allJavascriptFrontmatter.edges.length > 0
  ) {
    return (
      <div>
        {/* loop through each page */}
        {props.data.allJavascriptFrontmatter.edges.map((page, index) => {
          {
            /*
              do something with each page's data via the current 'page object':
                page.node.frontmatter.title
                page.node.frontmatter.role
                page.node.frontmatter.blurb
                etc.
            */
          }
        })}
      </div>
    );
  } else {
    return null;
  }
}
export default PortfolioItems;

Accessibility

Another feature I wanted to implement were some easily reusable components made with ARIA best practices built-in. An example of this is the Accordions component. When building this, I had the following goals:

  1. progressively enhance the component to ensure that all content is visible if JavaScript is not enabled
  2. make it easy to create a group of accordions
  3. ensure accordions can be added to any style of elements
  4. have the accordions follow ARIA best practices for accordions:
    • a header that controls showing/hiding content
    • a panel which is the content that shown/hidden
    • when one accordion group is opened, the rest are closed

Goal 1: Progressively Enhance

To accomplish goal 1, I decided that an accordion group should just display as a normal heading and content pair. If JavaScript is disabled, the content will appear to be organized normally by headings.

For the markup I settled on something similar to the section below.

import Accordions from "./Accordions";

function SomeComponent() {
  return (
    <Accordions>
      {/* start of accordion group 1 */}
      <button>Accordion Group Button 1</button>
      <p>Content for the 1st accordion group.</p>
      <p>Content for the 1st accordion group continued.</p>
      {/* end of accordion group 1 */}

      {/* start of accordion group 2 */}
      <button>Accordion Group Button 2</button>
      <p>Content for the 2nd accordion group.</p>
      <p>Content for the 2nd accordion group continued.</p>
      {/* end of accordion group 2 */}
    </Accordions>
  );
}

Using CSS, I would then style the buttons to appear as headings.

Goal 2: Make it Easy

Instead of passing multiple props to one Accordions component, I decided it would be easiest to have the component build each individual accordion group itself. This process was quite lengthy, but made implementing new Accordions components a breeze.

import React from "react";

class Accordions extends React.Component {
  constructor(props) {
    super(props);

    // bind component to methods
    this.toggle = this.toggle.bind(this);

    // create empty arrays to store individual accordion groups (button and content) and references to those groups
    this.accordionGroups = [];
    this.accordionGroupRefs = [];

    if (Array.isArray(this.props.children)) {
      // there is more than one child within this element

      // create separate array of children that we will manipulate to create the accordion groups
      let accordionsChildren = this.props.children;

      // determine if any children are buttons
      let containsButton = accordionsChildren.some(this.isButton);

      if (containsButton) {
        while (accordionsChildren.length > 0) {
          /*
            loop through each child
            1. identify the position of the next button
            2. store all the following children ending at the next button or the end of the children array.
            3. add references to the button and panel
            4. add button and panel to the accordionGroups array
            5. store references in references array
            6. remove the button and children elements from the array
            7. repeat
          */
        }
      }
    }
  }
}
export default Accordions;

For full implementation, see the full constructor method on the Accordions component. At this point, the component has now stored each accordion button-panel group and can now be setup to properly handle showing/hiding content.

Goal 3: Flexibility of Style

My plan was to use accordions in two places:

While the implementation of show/hide functionality is the same, the way they are displayed is quite different. This meant it would be best to allow specifying the classes that are added/removed depending on element visibility. To accomplish this, I added properties to the Accordions component:

  • classButtonToggle — the class added to a button when it's corresponding content panel is visible
  • classContent — the initial class added to a content panel since these elements are dynamically generated
  • classContentToggle — the class added to a content panel when it's visible
import Accordions from "./Accordions";

function SomeComponent() {
  return (
    <Accordions
      classButtonToggle="accordion__button--open"
      classContent="accordion__content"
      classContentToggle="accordion__content--visible"
    >
      {/* start of accordion group 1 */}
      <button>Accordion Group Button 1</button>
      <p>Content for the 1st accordion group.</p>
      <p>Content for the 1st accordion group continued.</p>
      {/* end of accordion group 1 */}

      {/* start of accordion group 2 */}
      <button>Accordion Group Button 2</button>
      <p>Content for the 2nd accordion group.</p>
      <p>Content for the 2nd accordion group continued.</p>
      {/* end of accordion group 2 */}
    </Accordions>
  );
}

Now, I can create a group of accordions using any combination of CSS classes and therefore any styling.

Goal 4: ARIA Best Practices

Once we had already accomplished dynamically creating accordion button-panel groups is goal 2, ensuring we follow ARIA best practices for accordions was easy.

Requirement: Keyboard Enter or Space

From the Web Accessibility Initiative (WAI)-ARIA best practices:

When focus is on the accordion header for a collapsed panel, expands the associated panel. If the implementation allows only one panel to be expanded, and if another panel is expanded, collapses that panel.

Buttons can be activated via the enter or space key by default.

Requirement: Keyboard Tab and Shift + Tab

From the WAI-ARIA best practices:

Tab: Moves focus to the next focusable element; all focusable elements in the accordion are included in the page Tab sequence.

Shift + Tab: Moves focus to the previous focusable element; all focusable elements in the accordion are included in the page Tab sequence.

Buttons are also part of the document's tab order by default so this is also already built-in.

Requirement: Roles

From the WAI-ARIA best practices:

Each accordion header button is wrapped in an element with role heading that has a value set for aria-level that is appropriate for the information architecture of the page.

This was simple update to the button markup we provide the Accordion component.

<button className="accordion__button" role="heading" aria-level="3">
  Button Text
</button>
Requirement: States

From the WAI-ARIA best practices:

If the accordion panel associated with an accordion header is visible, the header button element has aria-expanded set to true. If the panel is not visible, aria-expanded is set to false.

This was easily accomplished using React's state property.

// render of AccordionButton sub-component
render(){
  return(
    <this.props.element.type
      {...this.props.element.props}
      aria-expanded={this.state.open}
    >
      {this.props.element.props.children}
    </this.props.element.type>
  );
}

The open property is then updated depending on whether its associated panel is shown/hidden.

Requirement: Properties

From the WAI-ARIA best practices:

The accordion header button element has aria-controls set to the ID of the element containing the accordion panel content.

I actually already needed to create a unique ID for both the button and panel for the key property since I place them all in one array.

// code in the middle of the loop where we identify buttons and their corresponding panels

// generateId is a custom utility that will return a unique ID concatenated with an optional string
let contentKey = generateId("accordions__button");

// create AccordionContent sub-component (the panel)
let content = (
  <AccordionContent
    key={contentKey}
    ref={contentRef}
    childElements={accordionChildren}
  />
);

// create AccordionButton sub-component and pass the contentKey to be used as the aria-controls attribute
let button = (
  <AccordionButton
    key={generateId("accordions__button")}
    contentKey={contentKey}
    element={accordionButton}
    {...accordionButton.props}
  >
    {accordionButton.props.children}
  </AccordionButton>
);

From the WAI-ARIA best practices:

If the accordion panel associated with an accordion header is visible, and if the accordion does not permit the panel to be collapsed, the header button element has aria-disabled set to true.

Since my implementation will allow all panels to be hidden, this is not an issue.

Conclusion

In the end I feel that I was successful in accomplishing the accessibility implementation goals; however, I'm a bit hesitant to say that this would work for every situation.

In this process, I did make extensive use of React refs to make calling sub-component methods easier. For accordions in a single page application, I may combine the sub-components into a single component instead.