Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simplified color naming for accessibility #6180

Merged
merged 14 commits into from
Apr 12, 2024
Merged

Simplified color naming for accessibility #6180

merged 14 commits into from
Apr 12, 2024

Conversation

devongovett
Copy link
Member

@devongovett devongovett commented Apr 10, 2024

Closes #5792, closes #4110, closes #4111.

This implements an alternative color naming strategy to the one proposed in #5792 (and #1732 before that). The previous proposal used fairly complex color names that I'm not sure would be understandable for everyone, or easily translated to different languages. For example, I'm not sure I would know the difference between "Arctic Blue", "Cornflower Blue", "Cobalt Blue", or "Persian Blue" without looking at them. These names also resulted in the need for over 250 new strings that would need to be translated into different languages, and increase the bundle size.

This proposal uses a simplified strategy that requires only 26 additional strings, in various combinations. This includes 13 main hues (pink, red, orange, brown, yellow, green, cyan, blue, purple, magenta, gray, white, and black), along with the halfway points between them (e.g. red orange, yellow green, and blue purple), combined with modifiers for lightness (very dark, dark, light, and very light), and chroma (pale, grayish, and vibrant). These combine together to form a description for any color. In addition to reducing the number of strings we need, these descriptions are also simpler, more universally understood, and more easily translated to other languages. This proposal is inspired by how the native iOS color picker works in VoiceOver.

The algorithm used to generate the description works in the OKLCH color space, recently standardized by CSS. This color space has the advantage of having uniform lightness across all hues, unlike HSL/HSV for which some hues such as blue are much darker than others. As shown below, you can see the difference between HSL and OKLCH.

It also avoids a shift in hue based on lightness, as can be seen in HSL for blue (which shifts toward purple).

These properties mean we can produce a perceptually accurate description for any color. The algorithm first converts the color to OKLCH, and uses the manually picked thresholds along the l, c, and h channels to produce descriptions for the hue, lightness, and chroma. If a color is partially transparent, that is also prepended (I used percent transparent rather than percent opaque here because I thought it seemed more easily understandable).

In order to localize these color names, the strings for each channel are externalized, and a "colorName" string combines them together. For example in English, the string is "{transparency} {lightness} {chroma} {hue}", but in other languages, the words may be in a different order. In French, I believe the hue would come first, followed by the lightness and chroma, and in Chinese there would be no spaces between the words. The halfway points between hues (e.g. yellow orange) are also included as separate strings rather than combining the individual hues because I believe in some languages there are single words for these colors rather than combining the two words like in English. We will need to somehow explain the way this works to translators so they can accurately produce strings in each language.

To easily test, I've added the color name to the stories for ColorArea, ColorSlider, and ColorWheel, but they should also be read by screen readers.

@rspbot
Copy link

rspbot commented Apr 10, 2024

Copy link
Member

@reidbarber reidbarber left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested various combinations in Safari with VO and this seems much better!

}

let hue: string;
[hue, l] = this.getOklchHue(l, c, h, locale);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see l change as a result of this calculation, but I don't quite understand why. I know it's for orange because it dips into brown very quickly, but wouldn't the l already be correct because it's one of the channels returned by the conversion to OKLCH?

if we just got this from the sample conversion code, that's fine, I might've missed it while reading through

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It comes from this line: https://github.com/adobe/react-spectrum/pull/6180/files/c05e757846a4745b9e80cb2ec9fbf54e595abb77#diff-2737484a486e2634802c43f60406fc1231698480673f4268bd1e8f029448bcccR185

Basically, if the hue is orange, we need to adjust the lightness so that "dark orange" comes before "brown", so we split the lightness range in half. Otherwise orange would always be considered light/very light.

@rspbot
Copy link

rspbot commented Apr 12, 2024

@rspbot
Copy link

rspbot commented Apr 12, 2024

## API Changes

unknown top level export { type: 'any' }
unknown top level export { type: 'any' }
unknown top level export { type: 'any' }
unknown top level export { type: 'any' }
unknown top level export { type: 'any', access: 'private' }
unknown top level export { type: 'any', access: 'private' }
unknown top level export { type: 'any' }
unknown top level export { type: 'any' }
unknown top level export { type: 'any' }
unknown top level export { type: 'any' }
unknown top level export { type: 'any' }
unknown top level export { type: 'any' }
unknown top level export { type: 'identifier', name: 'Column' }
unknown top level export { type: 'identifier', name: 'Column' }
unknown type { type: 'link' }
unknown type { type: 'link' }
unknown type { type: 'link' }
unknown type { type: 'link' }
unknown type { type: 'link' }
unknown type { type: 'link' }
unknown top level export { type: 'any' }
unknown top level export { type: 'any' }
unknown top level export { type: 'any' }
unknown top level export { type: 'any' }
unknown top level export { type: 'any' }
unknown top level export { type: 'any' }

@react-stately/color

Color

changed by:

  • Color
 Color {
+  clone: () => Color
   formatChannelValue: (ColorChannel, string) => string
   getChannelName: (ColorChannel, string) => string
   getChannelRange: (ColorChannel) => ColorChannelRange
   getChannelValue: (ColorChannel) => number
   getColorChannels: () => [ColorChannel, ColorChannel, ColorChannel]
+  getColorName: (string) => string
   getColorSpace: () => ColorFormat
   getColorSpaceAxes: ({
     xChannel?: ColorChannel
   yChannel?: ColorChannel
 }) => ColorAxes
+  getHueName: (string) => string
   toFormat: (ColorFormat) => Color
   toHexInt: () => number
   toString: (ColorFormat | 'css') => string
   withChannelValue: (ColorChannel, number) => Color
 

it changed:

  • AriaColorFieldProps
  • SpectrumColorAreaProps
  • SpectrumColorFieldProps
  • SpectrumColorSliderProps
  • SpectrumColorWheelProps
  • Color
  • ColorAreaProps
  • ColorFieldProps
  • ColorWheelProps

@devongovett devongovett merged commit 9cbe7d4 into main Apr 12, 2024
25 checks passed
@devongovett devongovett deleted the color-name branch April 12, 2024 18:08
@majornista
Copy link
Collaborator

majornista commented Apr 16, 2024

@devongovett Curious why pure yellow #ffff00 returns the string "very light vibrant yellow green."

Note that "Arctic Blue", "Cornflower Blue", "Cobalt Blue", and "Persian Blue" are named colors in CSS 4, but I agree that they may not be useful when providing an accessibility name.

@devongovett
Copy link
Member Author

devongovett commented Apr 16, 2024

Good question. Might need to adjust the thresholds a bit: #6210

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
5 participants