UX For Theme and Plugin Developers – Mel Choyce – Design Engineerat Automattic



UX For Theme and Plugin Developers – Mel Choyce – Design Engineerat Automattic

0 1


UX-For-WP-Developers

WordCamp Boston 2013 Workshop: How to handle UX for plugin and theme developers. Covers settings, custom post types, custom taxonomies, etc.

On Github ryelle / UX-For-WP-Developers

UX For Theme and Plugin Developers

Mel Choyce [@melchoyce] • Kelly Dwan [@ryelle]

Mel Choyce

Design Engineerat Automattic

Kelly Dwan

Web Engineerat 10up

Intro to User Experience

What is Usability?

How easy something is to use and learn

Can tasks be completed without intense frustration?

What is UX?

UX goes a level above usability — it's about how enjoyable a product is. It's about creating a pleasurable experience for our users. It's about making our users' lives easier. Usability should just be the baseline, not the standard. We don't just want our users to not be frustrated, we want them to be positive, even delighted.

UX within wp-admin

So, how many of you have installed a theme or a plugin, and then had no idea what to do next to get it working?

Yeah, that probably wasn't a great experience for you.

Provide a familiar environment

Don't make people hunt

When you're adding features to wp-admin, there are two things you want to make sure you do.

Provide a familiar environment for your users to learn your new feature.

Don't make your users have to hunt around for things. Make them feel secure in knowing where things are.

Be one with WordPress

New features, such as plugins and themes, should always be seamlessly integrated into the WordPress admin interface. It should never look like you're leaving WordPress — the fact that you're an extension of WordPress should be totally invisible. Your theme or plugin should feel like it belongs.

Use the Settings API

When at all possible, you should be using the Settings API to create new admin pages for your plugins or themes.

Otherwise, if WordPress decides to go and, oh, totally change the admin styles... Your new pages look terrible.

Put your options in logical locations

Place your theme or plugin settings/configurations/actions in a logical location in the admin menu.

When to make top-level menus

Adding a top-level menu should only be considered if you really need multiple, related screens to make WordPress do something it was not originally designed to accomplish, or are adding a totally new content type. Examples of new top-level menus might include conference management — tickets, speakers, sponsors, etc.

Group your settings page in with this menu.

When to make sub-menus

If you don't need a top-level menu, decide which menu to add your plugin or theme options to. Most plugins add sub-level menu items within existing WordPress menus. For example, a backup plugin adds a page to the Tools top-level menu, or a gallery plugin could be added to the media top-level menu.

Pages should be labeled accurately so users will know what they will find on each page without having to guess. Keep labels short and concise.

Add your settings to existing settings pages, if it makes sense, otherwise add it as a sub-menu to the settings top-level menu.

Create custom icons

If you are adding a top-level menu, then it's highly recommended that you add a custom icon which stylistically blends in with the rest of the admin icons.

Your icon should, by default, be black and white, with color on hover.

:(

Settings API

Create a new page Tell WordPress about your settings Display the (empty) page Create a section on the page Create individual fields Validate & save these fields

Create a new page

add_action( 'admin_menu', 'wcbos_settings_page' );

function wcbos_settings_page() {
	add_options_page( 
		'WordCamp', // Page title
		'WCBos Example Settings', // Menu title
		'manage_options', // Capability
		'wcbos-2013', // Slug
		'wcbos_settings_page_render' // Display callback
	);
}

add_options_page()

There are a few other functions for this: add_dashboard_page(), add_posts_page(), add_media_page(), add_links_page(), add_pages_page(), add_comments_page(), add_theme_page(), add_plugins_page(), add_users_page(), add_management_page() [tools], add_options_page(). More generically, add_submenu_page() for any page, will also let you add to a CPT.

add_menu_page to add a page as a top-level item.

Tell WordPress about your settings

add_filter( 'admin_init', 'wcbos_register_fields' );

function wcbos_register_fields() {
	register_setting( 
		'wcbos_options', // Option group (used to display fields)
		'wcbos_option', // Option name
		'wcbos_validate' // Validation callback
	);
	
	// ... continued later ...
}

register_setting()

Later slides will continue this function (wcbos_register_fields), it's here that we set up all our fields.

Display the (empty) page

function wcbos_settings_page_render() {
	?>
	<div class="wrap">
		<h2>WordCamp Boston Options</h2>
		<form action="options.php" method="post">
			<?php settings_fields( 'wcbos_options' ); ?>
			<?php do_settings_sections( 'wcbos_options' ); ?>
			<?php submit_button(); ?>
		</form>
	</div>
	<?php
}

settings_fields()do_settings_sections()submit_button()

Create a section on the page (setup)

// ... inside wcbos_register_fields()
	add_settings_section( 
		'wcbos_first_section', // ID
		__( "Meetup API Settings" ), // Title
		'wcbos_settings_first_section', // Display callback
		'wcbos_options' // Page
	);
// ... more settings ...

add_settings_section()

Create a section on the page (display)

function wcbos_settings_first_section() {
	_e( "This is a description of the first section." );
}

You can also pass false to add_settings_section for the callback, to only display the section title.

Creating individual fields (setup)

// ... inside wcbos_register_fields()
	add_settings_field(
		'wcbos_text_one', // ID
		__( "First Name" ), // Title
		'wcbos_settings_text_field', // Display callback
		'wcbos_options', // Page
		'wcbos_first_section', // Section
		array( 'label_for' => 'wcbos_text_one' ) // Args
	);
// ... more settings ...

add_settings_field()

Creating individual fields (display)

function wcbos_settings_text_field( $args ) {
	if ( ! isset( $args['label_for'] ) )
		return;

	$id = $args['label_for'];
	$values = get_option( 'wcbos_option' );
	printf( 
		'<input type="text" name="%1$s" id="%1$s" value="%2$s" />', 
		"wcbos_option[$id]", 
		esc_attr( $values[$id] )
	);
}

We're using label_for as an ID, so we can create a "generic" text input function.

Creating a dropdown field (setup)

// ... inside wcbos_register_fields()
	add_settings_field(
		'wcbos_dropdown', // ID
		__( "Options" ), // Title
		'wcbos_settings_dropdown', // Display callback
		'wcbos_options', // Page
		'wcbos_first_section', // Section
		array( 'label_for' => 'wcbos_dropdown' ) // Args
	);
// ... more settings ...
}

function wcbos_example_options() {
	$options = array(
		'a' => __( "Option A" ),
		'b' => __( "Option B" ),
		'c' => __( "Option C" ),
	);
	return apply_filters( 'wcbos_example_options', $options );
}

Creating a dropdown field (display)

function wcbos_settings_dropdown( $args ) {
	if ( ! isset( $args['label_for'] ) )
		return;

	$id = $args['label_for'];
	$values = get_option( 'wcbos_option' );
	$options = wcbos_example_options(); // Filterable list of choices
	echo '<select name="wcbos_option['.$id.']">';
	foreach ( $options as $key => $view ) {
		printf( 
			'<option value="%s" %s>%s</option>', 
			$key, 
			selected( $key, $values[$id], false ), 
			$view 
		);
	}
	echo '</select>';
}

selected()

All of register

function wcbos_register_fields() {
	register_setting( 
		'wcbos_options', // Option group (used to display fields)
		'wcbos_option', // Option name
		'wcbos_validate' // Validation callback
	);

	add_settings_section( 
		'wcbos_first_section', // ID
		__( "Meetup API Settings" ), // Title
		'wcbos_settings_first_section', // Display callback
		'wcbos_options' // Page
	);

	add_settings_field(
		'wcbos_text_one', // ID
		__( "First Name" ), // Title
		'wcbos_settings_text_field', // Display callback
		'wcbos_options', // Page
		'wcbos_first_section', // Section
		array( 'label_for' => 'wcbos_text_one' ) // Args
	);

	add_settings_field(
		'wcbos_dropdown', // ID
		__( "Options" ), // Title
		'wcbos_settings_dropdown', // Display callback
		'wcbos_options', // Page
		'wcbos_first_section', // Section
		array( 'label_for' => 'wcbos_dropdown' ) // Args
	);
}

Saving/validating these fields

We've created a text field and a dropdown, now we should validate/sanitize before saving.

In register_setting we defined a validation callback wcbos_validate.

function wcbos_validate( $input ) {
   $output = array();

   // If set and valid
   if ( isset( $input['wcbos_text_one'] ) ){
      $output['wcbos_text_one'] = sanitize_text_field( $input['wcbos_text_one'] );
   }

   $options = wcbos_example_options();
   if ( isset( $input['wcbos_dropdown'] ) ){
      if ( in_array( $input['wcbos_dropdown'], array_keys( $options ) ) ) {
         $output['wcbos_dropdown'] = $input['wcbos_dropdown'];
      }
   }

   return $output;
}

sanitize_text_field()

You probably don't need a "themes options" page.

Most items you could put in a theme options page can go somewhere else.

Don’t take items that should be in the Appearance menu (like custom headers, background, etc.) and put them in your screen options page. They should stay as options in the Appearance menu. Also add them to the customizer!

Use the core Custom Header & Custom Background functions

add_theme_support( 'custom-background', $optional_defaults );
add_theme_support( 'custom-header', $optional_defaults );

Custom HeadersCustom Backgrounds

But I have special settings!

Have something like a logo uploader? Give it its own page, "Custom Logo", instead of "theme options". It's more intuitive, and if a user sees that, they know what it means.

Have something like a theme slider? That could also be given a uniquely titled page.

Creating a settings page under Appearance

add_action( 'admin_menu', 'wcbos_theme_page' );

function wcbos_theme_page() {
	add_theme_page( 
		'Custom Logo', // Page title
		'Custom Logo', // Menu title
		'manage_options', // Capability
		'custom-logo', // Slug
		'wcbos_theme_page_render' // Display callback
	);
}

Does this look familiar? If you've been paying attention, it should, as it's the same as the beginning of the settings API section.

add_theme_page()

Make better UI decisions

Let's talk a little bit about making good UI decisions. This is where we're going to go from your plugin or theme being just usable, to it being a great experience.

Make information more digestible

We stole this from Helen. Thanks Helen.

Make it easier for the user to find what they need

In this case, they had a bunch of different taxonomies that could be applied to their custom post type. Instead of having a huge jumble of checkbox lists, they consolidated it into one metabox with smarter selection using a library called Chosen. Then for age, instead of checkboxes, they replaced it with a slider.

Place elements in logical locations

Pick smarter locations:

  • Add item --> Button
  • Formatting --> Editor
  • Meta data --> Meta box

Tailor your screens

Remove what you don't need

Replace what you do (strings, etc.)

Don't overwhelm users with options

What are the options or features that 80% of your users need?

Hide the rest in screen options (and document where they are)

Respond to user's actions

If users do something, they're expecting to receive feedback — a hover state on a button, a success message, something — always provide your users with that kind of feedback.

This is also a great place to sneak in some personality — an icon that changes from to a fun spinner while you save, a happy face when you complete a task, etc.

"Experiencing an app without good design feedback is like getting your high-five turned down."

~ @strevat

Clearly mark alerts and messages

If you're telling your users something, generally it's something you want them to see. Don't let it fade into the background — make it another color, give it some special treatment, make it big... Just get their attention.

Write good error messages

  • Write for people, not machines
  • Explain what went wrong
  • Help users recover

Custom Content

Create a post type with the correct defaults Create a custom taxonomy Add any extra fields to the post type Update the post type icon Update any strings with relevant text

Create a post type with the correct defaults

add_action( 'init', 'wcbos_create_post_type' );

function wcbos_create_post_type() {
	register_post_type( 'wcbos-people', array(
		'labels'      => array(
			'name'          => 'People',
			'singular_name' => 'Person',
			'add_new'       => 'Add Person',
			'add_new_item'  => 'Add New Person',
			'edit_item'     => 'Edit Person',
			'new_item'      => 'New Person',
			'search_items'  => 'Search People',
			'not_found'     => 'No people found',
			'not_found_in_trash' => 'No people found in trash'
		),
		'public'      => true,
		'supports'    => array( 'title', 'editor', 'thumbnail' ),
		'rewrite'     => array( 'slug' => 'person' ),
		'has_archive' => 'people',
	) );
	// ... continued

Title, editor, featured image

register_post_type()

Generally post types are singular names, this was a mistake I realized last night.

Create a Custom Taxonomy

// ... inside wcbos_create_post_type()
	register_taxonomy( 'wcbos-team', 'wcbos-people', array(
		'labels' => array(
			'name' => 'Teams',
			'singular_name' => 'Team',
			'all_items' => 'All Teams',
			'edit_item' => 'Edit Team',
			'view_item' => 'View Team',
			'update_item' => 'Update Team',
			'add_new_item' => 'Add New Team',
			'new_item_name' => 'New Team Name',
			'search_items' => 'Search Teams',
			'popular_items' => 'Popular Teams',
			'parent_item' => 'Parent Team',
			'parent_item_colon' => 'Parent Team:',
		),
		'hierarchical' => true,
		'show_admin_column' => true,
		'rewrite' => array( 'slug' => 'team' ),
	) );
}

register_taxonomy()

Extra Fields on a Post Type (setup)

// inside the register_post_type args in wcbos_create_post_type()
		'has_archive' => 'people',
		'register_meta_box_cb'=> 'wcbos_add_metaboxes',
	) );
}

function wcbos_add_metaboxes() {
	// New metabox for person information: twitter, website, etc
	add_meta_box( 
		'wcbos-people-info', // ID
		__( "Social Media" ), // Title
		'wcbos_people_info_box', // Display callback
		'wcbos-people', // Post type
		'normal', // Context, 'normal', 'advanced', or 'side'
		'default' // Priority, 'high', 'core', 'default' or 'low'
	);
	
	// ... continued later ...
}

add_meta_box()

Extra Fields on a Post Type (display)

function wcbos_people_info_box() {
	$twitter = get_post_meta( $post->ID, 'twitter', true );
	?>
	<p>
		<label for="info_twitter">Twitter:</label> 
		<input type="text" name="info_twitter" value="<?php echo esc_attr( $twitter ); ?>" id="info_twitter" />
	</p>
	<?php
}

get_post_meta()

Extra Fields on a Post Type (saving)

add_action( 'save_post', 'wcbos_save_post_meta' );

function wcbos_save_person_meta( $post_id ) {
	if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE )
		return;

	if ( ! current_user_can( 'edit_post', $post_id ) )
		return;

	if ( ! isset( $_POST['person-info'] ) || ! wp_verify_nonce( $_POST['person-info'], 'save' ) )
		return;

	if ( isset( $_POST['info_twitter'] ) ){
		update_post_meta( 
			$post_id, // Post ID
			'twitter', // Meta key
			sanitize_text_field( $_POST['info_twitter'] ) // Value
		);
	}

}

save_postupdate_post_meta()

Renaming the Featured Image

// inside wcbos_add_metaboxes()
	remove_meta_box( 
		'postimagediv', // ID
		'wcbos-people', // Post type
		'side' // Context
	);
	add_meta_box( 
		'postimagediv', 
		__( 'Photo' ), 
		'post_thumbnail_meta_box', 
		'wcbos-people', 
		'side' 
	);
}

remove_meta_box()

Update the Post Type Icon (non-MP6)

add_action( 'admin_head', 'wcbos_custom_icons' );

function wcbos_custom_icons() { ?>
	<style type="text/css" media="screen">
		#adminmenu #menu-posts-wcbos-people .wp-menu-image {
			/* Sprite for non-MP6 */
			background-image: url(<?php echo plugins_url('admin-users.png',__FILE__); ?>);
			background-position: 5px 4px;
		}
		#adminmenu #menu-posts-wcbos-people.wp-has-current-submenu .wp-menu-image,
		#adminmenu #menu-posts-wcbos-people a:hover .wp-menu-image {
			background-position: 5px -23px;
		}
	</style>
<?php }

Update the Post Type Icon (MP6)

add_action( 'admin_head', 'wcbos_custom_icons' );

function wcbos_custom_icons() { ?>
	<style type="text/css" media="screen">
	.mp6 #adminmenu #menu-posts-wcbos-people .wp-menu-image:before {
		content: '\f307';
	}
	</style>
<?php }

More examples, using custom font & SVG

Update Strings With Relevant Text

<?php
add_filter( 'post_updated_messages', 'wcbos_people_updated_messages' ) );

function wcbos_people_updated_messages( $messages = array() ) {
	global $post, $post_ID;
	
	$messages['wcbos-people'] = array(
		0  => '', // Unused. Messages start at index 1.
		1  => sprintf( __( 'Person updated. <a href="%s">View person</a>.', 'textdomain' ), esc_url( get_permalink( $post_ID ) ) ),
		2  => __( 'Custom field updated.', 'textdomain' ),
		3  => __( 'Custom field deleted.', 'textdomain' ),
		4  => __( 'Person updated.', 'textdomain' ),
		/* translators: %s: date and time of the revision */
		5  => isset( $_GET['revision'] ) ? sprintf( __( 'Person restored to revision from %s', 'textdomain' ), wp_post_revision_title( (int) $_GET['revision'], false ) ) : false,
		6  => sprintf( __( 'Person published. <a href="%s">View slide</a>', 'textdomain' ), esc_url( get_permalink( $post_ID ) ) ),
		7  => __( 'Person saved.', 'textdomain' ),
		8  => sprintf( __( 'Person submitted. <a target="_blank" href="%s">Preview slide</a>', 'textdomain' ), esc_url( add_query_arg( 'preview', 'true', get_permalink( $post_ID ) ) ) ),
		9  => sprintf( __( 'Person scheduled for: <strong>%1$s</strong>. <a target="_blank" href="%2$s">Preview person</a>', 'textdomain' ),
			// translators: Publish box date format, see http://php.net/date
			date_i18n( __( 'M j, Y @ G:i', 'textdomain' ), strtotime( $post->post_date ) ), esc_url( get_permalink( $post_ID ) ) ),
		10 => sprintf( __( 'Person draft updated. <a target="_blank" href="%s">Preview person</a>', 'textdomain' ), esc_url( add_query_arg( 'preview', 'true', get_permalink( $post_ID ) ) ) ),
	);
	
	return $messages;
}

post_updated_messages

Update Strings With Relevant Text

<?php
add_filter( 'enter_title_here', 'wcbos_people_enter_title', 10, 2 );

function wcbos_people_enter_title( $text, $post ) {
	if ( $post->post_type == 'wcbos-people' )
		$text = __( "Enter name here" );
	
	return $text;
}

enter_title_here

User Testing

What

A formalized method for testing your product with your user group

Qualitative, usually pretty informal

Why

Cheap

Easy

Fast

You can do it cheap, it's easy, and it's fast.

All sites/products have problems. Most serious problems get identified pretty quickly — Great way to identify the biggest usability issues in your product. Usually also identifies a lot of quick wins to fix

It makes you a better creator

How

Recruit to fill 3-5 tests

Steve Krug says 3, Jakob Nielsen says 5, but even 1-2 are better than 0

Write your tasks

Tasks are things like: Install plugin. Configure plugin to do X thing. Check to see if it is working. etc.

Should be the most important features of your product

Turn tasks into scenarios

Scenario: you're a photo blogger and you want to install a plugin which enhances WordPress' internal galleries. You've heard x plugin is great. Install x plugin to your WordPress site.

Test run your test internally

Record screen

Lots of services to do this: camtasia, silverback, etc.

Don't explain, just watch

If your users asks you what to do or are confused, ask them where they think they'd be able to do x

You can test remotely, too

For wp core and wpcom, we use usertesting.com

Rocket Surgery Made Easy

Let's do it!

Write down or start thinking of a bunch of tasks users face when using your theme or plugin Narrow it down to 3-5 tasks Start writing scenarios based on those tasks Let's test with someone!

THE END