Saturday, July 31, 2021

ui gripe blog: not preserving zoom across modes in iphone's photo app

All too common thing for me: see a possible candidate for my "One Second Everyday" project, fire up the camera app, zoom-in, notice I'm on photo, switch to video-- but the zoom resets and I have to zoom-in again. 

A more mature UI would recognize "just zoomed in/out, then switched modes" as a request to say "show me this same shot but in the different mode". There are some technical gotchas across modes but still.

Sometimes I think I'm the only person in the world who cares about UX ;-) 

Thursday, July 29, 2021

moving common sibling nieces leaf properties up to be... siblings? in a map

 So this is almost harder to describe than show, but we have a big map like object. CSS properties for components are at the leaves, so for instance "button.square.lg.pressed" might have the CSS key/values for the pressed state. But that leads to a lot of duplication, like maybe "button.square.lg.idle" is the same except for the color or something. So we'd like to move the actual properties up to point where every child has them in common, so 

{
 "button": {
  "idle": {
   "margin": "16px 8px",
   "backgroundColor": "white"
  },
  "hover": {
   "margin": "16px 8px",
   "backgroundColor": "lightblue"
  },
  "focus": {
   "margin": "16px 8px",
   "backgroundColor": "darkblue"
  }
 }
}

might become

{
 "button": {
  "idle": {
   "backgroundColor": "white"
  },
  "hover": {
   "backgroundColor": "lightblue"
  },
  "focus": {
   "backgroundColor": "darkblue"
  }
 },
 "margin": "16px 8px"
}

like that.
A coworker basically did the work (using lodash's isObject and isEqual) - here is a codepen using lodash and typescript, the code is 

// import { isObject, isEqual } from '/lodash';

const hasPlainChildren = (obj: any) => {
  for (const key of Object.keys(obj)) {
    const child = obj[key] as any;
    if (!_.isObject(child)) continue;
    for (const childKey of Object.keys(child)) {
      const newChild = (child as any)[childKey];
      if (_.isObject(newChild)) return false;
    }
  }
  return true;
};

const promoteValues = (obj: any, ignoreList: string[]) => {
  const newObj = { ...obj };
  const items = [] as { key: string; value: any; presentIn: string[] }[];
  if (!_.isObject(newObj)) {
    return newObj;
  }
  for (const key of Object.keys(newObj)) {
    const child = (newObj as any)[key];
    if (!_.isObject(child)) continue;
    for (const prop in child) {
      if (_.isObject((child as any)[prop])) continue;
      const valueInSet = items.find(
        (item) => item.key === prop && item.value === (child as any)[prop],
      );
      if (valueInSet) {
        valueInSet.presentIn.push(key);
      } else {
        items.push({
          key: prop,
          value: (child as any)[prop],
          presentIn: [key],
        });
      }
    }
  }
  for (const item of items.filter((item) =>
    _.isEqual(
      item.presentIn,
      Object.keys(newObj).filter((key) => !ignoreList.includes(key)),
    ),
  )) {
    (newObj as any)[item.key] = item.value;
    for (const key of Object.keys(newObj)) {
      const child = (newObj as any)[key] as any;
      if (_.isObject(child)) {
        (child as any)[item.key] = undefined;
      }
    }
  }
  return newObj;
};

export const simplifyObject = (obj: object, ignoreList: string[]) => {
  let newObject = { ...obj } as any;
  if (!hasPlainChildren(newObject)) {
    for (const key of Object.keys(newObject)) {
      if (!_.isObject(newObject[key])) continue;
      newObject[key] = simplifyObject(newObject[key], ignoreList);
    }
  }
  newObject = promoteValues(newObject, ignoreList);
  return newObject;
};
const foo = {
  button: {
    idle: {
      margin: "16px 8px",
      backgroundColor: "white"
    },
    hover: {
    margin: "16px 8px",
    backgroundColor: "lightblue"
  },
  focus: {
    margin: "16px 8px",
    backgroundColor: "darkblue"
  }
}
};

window.time.innerHTML = Date.now(); 
window.before.innerHTML = JSON.stringify(foo, null, ' ');
window.after.innerHTML = JSON.stringify(simplifyObject(foo,[]), null, ' ');

with this bit of HTML to show results:

<pre id="time"></pre>
before:
<pre id="before"></pre>
after:
<pre id="after"></pre>

Haven't really poured over the code but it seems to do the job!

Wednesday, July 28, 2021

WiP: showing state combinations of components

This is a work in progress, and is probably the wrong idea in general (see earlier and later posts) but I'm kind of proud of the concept...

One thing I am adding to my team is the idea of making things visible, so that dev testing and QA is easier, seeing things work in context. (For example I made a "formik playground" page in storybook so we could see our components work in formik context, still work out of that context, and then how formik "regular" fields would work so we could compare behaviors etc)

Related to that, my team has realized we sort of painted outselves in a corner in terms of not thinking about different possible states of components (error, disabled, selected, etc) function together. Most of these are "boolean" properties so I thought a simple grid could let us see all the two-fold combinations, how the states play together:

The end result looked like:



And the code (not quite perfected, especially from the typescripting complaints side:)


import React from 'react';
import PropTestingProps from './prop-testing.props';
import PropTestingComponent from './prop-testing.style';
import { Button } from '../Button/button.component';

import { Checkbox } from '../Checkbox/checkbox.component';
import { Radio } from '../Radio/radio.component';
import styled from 'styled-components';

const Table = styled.table`
  th {
    font-weight: bold;
  }
  th, td {
    padding: 8px;
    font-family: sans-serif;
  }
`;

interface ComponentPropMatrixProps {
  label: string,
  permutations: string[],
  render: React.FC
}
interface ComponentPropRowProps extends ComponentPropMatrixProps {
  rowEntry: string,
  permutations: string[],
  render: React.FC
}

const ComponentPropRow = (props: ComponentPropRowProps) => {
  const {permutations, rowEntry, render} = props;
  
  return (<tr>{permutations.map((colEntry: string)=>{
    const componentProps = {[colEntry] : true, [rowEntry]: true };
    return (<td key={colEntry}>
      {render({componentProps,label:`${rowEntry}`})}
        </td>);
  })}</tr>);
}

const ComponentPropMatrix = (props: ComponentPropMatrixProps) => {
  const {permutations, render, label} = props;
  return <>
    <h3>{label}</h3>
    <Table>
    <thead>
      <tr>
        {permutations.map(x => <th><b>{x} +</b></th>)}
      </tr>
    </thead>
    <tbody>
      {permutations.map((x,i) => {return <ComponentPropRow key={x} 
          render={render} permutations={permutations} rowEntry={x}/>})}
    </tbody>
    </Table>
    </>

}
// idle, checked, error, disabled

const PropTesting = (props: PropTestingProps) => {
  return <PropTestingComponent>
    <ComponentPropMatrix label="Checkbox" 
      permutations={['idle', 'checked','error','disabled']} 
      render={({componentProps,label})=><Checkbox {...componentProps} 
        label={label} />} />

    <ComponentPropMatrix label="Radio" 
       permutations={['idle', 'checked','error','disabled']} 
      render={({componentProps,label})=><Radio {...componentProps} 
         id={`${JSON.stringify(componentProps)}!`} label={label} />} />


    <ComponentPropMatrix label="Button sm primary" 
      permutations={[`'idle'`,'disabled']} 
      render={({componentProps,label})=><Button size="sm" 
      buttonType="primary" {...componentProps}>{label}</Button>} />
    <ComponentPropMatrix label="Button md tertiary" 
      permutations={[`'idle'`,'disabled']} 
      render={({componentProps,label})=><Button size="md" 
      buttonType="tertiary" {...componentProps}>{label}</Button>} />
    <ComponentPropMatrix label="Button lg oval" 
      permutations={[`'idle'`,'disabled']} 
      render={({componentProps,label})=><Button size="lg"  
      shape="oval" {...componentProps}>{label}</Button>} />

  </PropTestingComponent>;
};

export default PropTesting;

javascript get all subsets of an array, then sort subsets by length, and preserve order

This stackoverflow  had an elegant all permutations generator:

const getAllSubsets = 
      theArray => theArray.reduce(
        (subsets, value) => subsets.concat(
         subsets.map(set => [value,...set])
        ),
        [[]]
      );
const elems = [1,2,3];
console.log(JSON.stringify(getAllSubsets(elems)));

I can't say I really understand how it works! But here is the output:

[[],[1],[2],[2,1],[3],[3,1],[3,2],[3,2,1]]

I realized I would like it better if we sorted by length... the empty set, each single entry set, each 2 entry set... 

console.log(JSON.stringify(
      getAllSubsets(elems)
          .sort((a,b)=>a.length - b.length)
));

That made

[[],[1],[2],[3],[2,1],[3,1],[3,2],[3,2,1]]

Much better! But still, it would be cool if each individual array was sorted - not on the value but based on the order in the original set:

So I end up making

console.log(JSON.stringify(
      getAllSubsets(elems)
          .sort((a,b)=>a.length - b.length)
          .map(arr=>arr.sort((a,b)=>elems.indexOf(a) - elems.indexOf(b)  ))
));

That's cool! let me change elems to ["Foo", "Bar", "Baz"]:

[[],['Foo'],['Bar'],['Baz'],['Foo','Bar'],['Foo','Baz'],['Bar','Baz'],['Foo','Bar','Baz']]

Perfect!

UPDATE: the Typescript version that function is

const getAllSubsets = (theArray:string[]) => theArray.reduce(
        (subsets, value) => subsets.concat(
          subsets.map(set => [value,...set])
        ), [[] as string[]]
);

Monday, July 26, 2021

the ux of preconceptions

Besides Facebook (which, despite its many flaws, does the best job of keeping me in touch with people I know in real life but not well enough to be in touch by other means) my favorite form of social media is tumblr. 

When I mention that to people, the usual response I get is "is that still around?" which is kind of sad. Splitting the difference between (old) LiveJournal's long-form blogging and Instagram's reliance on images, it has a solid socially conscious community. (Like Twitter, it's really important who you follow, and unlike Twitter, you mostly just see the people you follow, in the order they post things.)

Anyway, one of my favorite tumblr sources is David J Prokopetz. Mostly I love his extensive riffs on tabletop games but also I thought this was a pretty good set of thoughts on UX in terms of user's preconceived mental models:

I guess a lot of my skepticism regarding user-focused design stems from working in tech support and witnessing first hand that user interfaces can only guide people to perform a task in a particular way if the method you have in mind is already very close to their prior assumptions about how the task ought to be performed.

If the way your UI thinks the task ought to be carried out and the way the user thinks the task ought to be carried out are very different, nine times out of ten what happens is that the user will Rube Goldberg together some bizarre workaround that’s ten times as complicated, takes ten times as long, and fights against the interface every step of the way, but preserves their prior assumptions, then become emotionally committed to their workaround and respond to any effort to demonstrate a more straightforward approach with extreme and disproportionate anger.

Critically, there doesn’t seem to be any way to avoid this scenario using pure design, because different people will have mutually exclusive prior assumptions about how the task ought to be performed, and you can’t possibly accommodate all of them. Conscientious design practices can supplement explaining shit in words, but they can’t replace it.

Good stuff!

Saturday, July 24, 2021

different mediums for better messages

Thinking on "body neutrality" (as a healthier and more realistic alternative to "body positivity") reminded me of a quip I made years ago - I've been trying to note when I remember early, nascent forms of my current philosophical stances - and I looked it up in my blog:



One of my favorite tags on my blog is /tag/aim, (mostly) bits from the old AOL Instant Messenger days. For a while I assumed it was mostly nostalgia that made me think "damn, we were funnier then" (or maybe just being a bit younger and more quick-witted after all!) but you know? The modern "equivalents" of AIM - SMS/WhatsApp etc... most of them are phone based. And it's much more challenging to get banter going between people tapping into their screens than with two competent typists!

Thursday, July 22, 2021

simplenote lifehack: "findme" as searchable string for important notes

 For at least 7 or 8 years I've been using (and increasingly relying on) "Simplenote", a hyper-minimalist text only note taking app that is EXTREMELY good at synching to my desktop and iOS app (and has a webclient as well.)

Simplenote doesn't offer folders. I've grown to love its "just a big pile of notes" approach - and to not be overly worried about separating the what from the chaff. There is a "tag" system, but I find it a bit cumbersome. Also for a while I would "pin" the important notes to the top, but that grew unwieldy as well.

I think I've found a great solution: for important notes I add "FINDME" to the title. This integrates much more smoothly with the speedy search bar than the tag based approached, letting me pluck out the relevant note from all the others with similar content just by typing.