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.

Tuesday, July 20, 2021

formik/yup problem: validating wrong value

 Odd geeky problem from work, we have components that support Formik - specifically if they find themselves in a FormikContext they take their values from there.

So one thing that's easy to miss if you're not paying attention is Formik components probably shouldn't display their error message unless they have been "touched" (submitting the form counts as a "touch" for each element. And of course, if you are making your own components, they should set "touched" once they are interacted with - the easiest way is to use the FormikContext.handleBlur(event), but some of our made up components - specifically some radio buttons - didn't have the "event" needed (a non-input DOM element was handling the click event), so I had to call FormikContext.setFieldTouched() directly. 

But we were getting weird results... we had Yup have the field as required, and even though a radio value was selected the error message for Required was still showing up.

We'd already built a "Formik Peek" component:

const FormikPeek = () =&gt; {
  const formikContext = useContext(FormikContext);
  const valuesString = formikContext &amp;&amp; JSON.stringify(formikContext.values);
  const errorsString = formikContext &amp;&amp; JSON.stringify(formikContext.errors);
  const touchedString = formikContext &amp;&amp; JSON.stringify(formikContext.touched);
  return (
    <table>
      <tbody>
        <tr>
          <th>values</th>
          <td>{valuesString}</td>
        </tr>
        <tr>
          <th>errors</th>
          <td>{errorsString}</td>
        </tr>
        <tr>
          <th>touched</th>
          <td>{touchedString}</td>
        </tr>
      </tbody>
    </table>
  );
};

And that was showing that the value was being updated ok, but Yup was being run.

So this is what we had as our onChange equivalent:

  const localOnChange: React.ChangeEventHandler<HTMLInputElement> = (event) => {
    onChange && onChange(event);
    formikContext && formikContext.setFieldValue(componentName, value);
    formikContext && formikContext.setFieldTouched(componentName, true, true);
  };

So, the trouble was the second "true" to setFieldTouch, which says "validate this now". That was somehow being read as "validate with the old value"- maybe because the setFieldValue and setFieldTouched was in the same thread? I dunno. But even with validation set to false, it still seemed to validate at about the right time, so I'm not sure when "true" is useful. 

Monday, July 19, 2021

JSON prettifier

 Oh this is cool - FracturedJSON gives you a more reasonable layout for your JSON:


JSON.stringify(value, null, ' '); tends to spread array contents across multiple lines - a bit better than your whole structure as one big old string, but not very efficient to skim.

(Via Javascript Weekly which I recommend for everyone doing Javascript)

Thursday, July 8, 2021

the potential trauma of "on this day"

Lauren Goode on I Called Off My Wedding. The Internet Will Never Forget - now that the Internet is a memory-aid for so many of us - and specifically, via tools on social media that has its own agenda for keeping people engaged - the potential for opening up old wounds is huge.

A lot of the topic is "On This Day", which is a common way to review old times when using a digital memory keeper. I've had a This Day in Kirkstory feature on my website for a long time. (Most of my memories, even the potential painful ones, have the analgesic of nostalgia.)

Thinking about it, the personal "On This Date" feature is kind of new and weird! I guess birthdays have always had a bit of it, and newspapers have long had "this date in history" features, but when used as a larger cross-section of life, the arbitrary nature of it emerges. It really leans into the cyclical / seasonal nature of life. Why should July 8ths across a life otherwise be seen as having stuff in common?

(Interestingly, Apple's "Memories" feature doesn't seem stuck in that rut. It keeps its cards pretty close to its chest in how it decides to curate things.) 

Tuesday, July 6, 2021

making space invaders modern

Bringing (space-invaders) emulation into the 21st century: Implementing an 8080 emulator in a microservice architecture on top of kubernetes 

This kind of abstraction overkill reminds me of the over-engineering of programming Pac-Man in a pure functional kind of way.