instructure-ui icon indicating copy to clipboard operation
instructure-ui copied to clipboard

fix(ui-selectable,ui-select): fix typing of Select and Selectable eve…

Open ToMESSKa opened this issue 1 month ago • 1 comments

…nt types and TypeScript errors in the examples

INSTUI-4866

ISSUE:

  • events in Select are wrongly typed
  • examples in Select therefore show TypeScript errors

TEST PLAN:

  • open the branch locally
  • copy these code examples from README that use event.key or event.keyCode into a new .tsx file in your local repository, they should not show TypeScript errors.
import React, { useState, useRef } from 'react'
import { Select } from '@instructure/ui-select'

/**
 * This file tests all the README examples that use event.key or event.keyCode
 * to ensure they work correctly with TypeScript after our type fixes.
 */

// Example 1: SingleSelectExample (line 47 in README)
export const SingleSelectExample = ({ options }: { options: Array<{ id: string; label: string }> }) => {
  const [inputValue, setInputValue] = useState(options[0].label)
  const [isShowingOptions, setIsShowingOptions] = useState(false)
  const [highlightedOptionId, setHighlightedOptionId] = useState<string | null>(null)
  const [selectedOptionId, setSelectedOptionId] = useState(options[0].id)

  const handleShowOptions = (event: React.KeyboardEvent | React.MouseEvent) => {
    setIsShowingOptions(true)
    if (inputValue || selectedOptionId || options.length === 0) return

    // This is the exact code from the README with type guard
    if ('key' in event) {
      switch (event.key) {
        case 'ArrowDown':
          return console.log('Arrow down')
        case 'ArrowUp':
          return console.log('Arrow up')
      }
    }
  }

  const handleHighlightOption = (
    event: React.KeyboardEvent | React.MouseEvent,
    { id }: { id?: string; direction?: 1 | -1 }
  ) => {
    event.persist()
    setHighlightedOptionId(id || null)
  }

  return (
    <Select
      renderLabel="Single Select"
      inputValue={inputValue}
      isShowingOptions={isShowingOptions}
      onRequestShowOptions={handleShowOptions}
      onRequestHighlightOption={handleHighlightOption}
    >
      {options.map((option) => (
        <Select.Option
          key={option.id}
          id={option.id}
          isHighlighted={option.id === highlightedOptionId}
          isSelected={option.id === selectedOptionId}
        >
          {option.label}
        </Select.Option>
      ))}
    </Select>
  )
}

// Example 2: AutocompleteExample (line 237 in README)
export const AutocompleteExample = ({ options }: { options: Array<{ id: string; label: string }> }) => {
  const [inputValue, setInputValue] = useState('')
  const [isShowingOptions, setIsShowingOptions] = useState(false)
  const [selectedOptionId, setSelectedOptionId] = useState<string | null>(null)

  const handleShowOptions = (event: React.KeyboardEvent | React.MouseEvent) => {
    setIsShowingOptions(true)
    if (inputValue || selectedOptionId || options.length === 0) return

    // Exact code from README with type guard
    if ('key' in event) {
      switch (event.key) {
        case 'ArrowDown':
          return console.log('Arrow down')
        case 'ArrowUp':
          return console.log('Arrow up')
      }
    }
  }

  return (
    <Select
      renderLabel="Autocomplete"
      inputValue={inputValue}
      isShowingOptions={isShowingOptions}
      onRequestShowOptions={handleShowOptions}
    >
      {options.map((option) => (
        <Select.Option key={option.id} id={option.id}>
          {option.label}
        </Select.Option>
      ))}
    </Select>
  )
}

// Example 3: MultipleSelectExample (lines 443 and 504 in README)
export const MultipleSelectExample = ({ options }: { options: Array<{ id: string; label: string }> }) => {
  const [inputValue, setInputValue] = useState('')
  const [isShowingOptions, setIsShowingOptions] = useState(false)
  const [selectedOptionId, setSelectedOptionId] = useState<string[]>(['opt1', 'opt6'])

  const handleShowOptions = (event: React.KeyboardEvent | React.MouseEvent) => {
    setIsShowingOptions(true)
    if (inputValue || options.length === 0) return

    // Exact code from README with type guard
    if ('key' in event) {
      switch (event.key) {
        case 'ArrowDown':
          return console.log('Arrow down')
        case 'ArrowUp':
          return console.log('Arrow up')
      }
    }
  }

  const handleKeyDown = (event: React.KeyboardEvent) => {
    // Exact code from README with type guard (line 504)
    if ('keyCode' in event && event.keyCode === 8) {
      // when backspace key is pressed
      if (inputValue === '' && selectedOptionId.length > 0) {
        // remove last selected option, if input has no entered text
        setSelectedOptionId(selectedOptionId.slice(0, -1))
      }
    }
  }

  return (
    <Select
      renderLabel="Multiple Select"
      inputValue={inputValue}
      isShowingOptions={isShowingOptions}
      onRequestShowOptions={handleShowOptions}
      onKeyDown={handleKeyDown}
    >
      {options.map((option) => (
        <Select.Option key={option.id} id={option.id}>
          {option.label}
        </Select.Option>
      ))}
    </Select>
  )
}

// Example 4: GroupSelectExample (line 669 in README)
export const GroupSelectExample = ({
  options
}: {
  options: Record<string, Array<{ id: string; label: string }>>
}) => {
  const [inputValue, setInputValue] = useState('')
  const [isShowingOptions, setIsShowingOptions] = useState(false)

  const handleShowOptions = (event: React.KeyboardEvent | React.MouseEvent) => {
    setIsShowingOptions(true)
    if (inputValue || Object.keys(options).length === 0) return

    // Exact code from README with type guard
    if ('key' in event) {
      switch (event.key) {
        case 'ArrowDown':
          return console.log('Arrow down - first group option')
        case 'ArrowUp':
          return console.log('Arrow up - last group option')
      }
    }
  }

  return (
    <Select
      renderLabel="Group Select"
      inputValue={inputValue}
      isShowingOptions={isShowingOptions}
      onRequestShowOptions={handleShowOptions}
    >
      {Object.keys(options).map((key, index) => (
        <Select.Group key={index} renderLabel={key}>
          {options[key].map((option) => (
            <Select.Option key={option.id} id={option.id}>
              {option.label}
            </Select.Option>
          ))}
        </Select.Group>
      ))}
    </Select>
  )
}

// Example 5: GroupSelectAutocompleteExample (line 853 in README)
export const GroupSelectAutocompleteExample = ({
  options
}: {
  options: Record<string, Array<{ id: string; label: string }>>
}) => {
  const [inputValue, setInputValue] = useState('')
  const [isShowingOptions, setIsShowingOptions] = useState(false)
  const [selectedOptionId, setSelectedOptionId] = useState<string | null>(null)

  const handleShowOptions = (event: React.KeyboardEvent | React.MouseEvent) => {
    setIsShowingOptions(true)
    if (inputValue || selectedOptionId || Object.keys(options).length === 0) return

    // Exact code from README with type guard
    if ('key' in event) {
      switch (event.key) {
        case 'ArrowDown':
          return console.log('Arrow down')
        case 'ArrowUp':
          return console.log('Arrow up')
      }
    }
  }

  return (
    <Select
      renderLabel="Group Select with autocomplete"
      inputValue={inputValue}
      isShowingOptions={isShowingOptions}
      onRequestShowOptions={handleShowOptions}
    >
      {Object.keys(options).map((key, index) => (
        <Select.Group key={index} renderLabel={key}>
          {options[key].map((option) => (
            <Select.Option key={option.id} id={option.id}>
              {option.label}
            </Select.Option>
          ))}
        </Select.Group>
      ))}
    </Select>
  )
}

// Example 6: Option Icons Example (line 1218 in README)
export const OptionIconsExample = ({ options }: { options: Array<{ id: string; label: string }> }) => {
  const [inputValue, setInputValue] = useState(options[0].label)
  const [isShowingOptions, setIsShowingOptions] = useState(false)
  const [selectedOptionId, setSelectedOptionId] = useState(options[0].id)

  const handleShowOptions = (event: React.KeyboardEvent | React.MouseEvent) => {
    setIsShowingOptions(true)
    if (inputValue || selectedOptionId || options.length === 0) return

    // Exact code from README with type guard
    if ('key' in event) {
      switch (event.key) {
        case 'ArrowDown':
          return console.log('Arrow down')
        case 'ArrowUp':
          return console.log('Arrow up')
      }
    }
  }

  return (
    <Select
      renderLabel="Option Icons"
      inputValue={inputValue}
      isShowingOptions={isShowingOptions}
      onRequestShowOptions={handleShowOptions}
    >
      {options.map((option) => (
        <Select.Option
          key={option.id}
          id={option.id}
          isSelected={option.id === selectedOptionId}
        >
          {option.label}
        </Select.Option>
      ))}
    </Select>
  )
}

// Test with inferred types (most common usage pattern)
export const InferredTypesExample = () => {
  const [isShowingOptions, setIsShowingOptions] = useState(false)

  return (
    <Select
      renderLabel="Inferred Types"
      isShowingOptions={isShowingOptions}
      // Inline handler with inferred types - this is how most users write it
      onRequestShowOptions={(event) => {
        setIsShowingOptions(true)

        // Type guard needed for union type
        if ('key' in event) {
          switch (event.key) {
            case 'ArrowDown':
              console.log('Down')
              break
            case 'ArrowUp':
              console.log('Up')
              break
          }
        }
      }}
      // Test FocusEvent compatibility
      onRequestHideOptions={(event) => {
        setIsShowingOptions(false)
        // Can access common properties
        console.log(event.type)

        // Can check for keyboard-specific properties
        if ('key' in event) {
          console.log('Keyboard event:', event.key)
        }

        // Can check for mouse-specific properties
        if ('button' in event) {
          console.log('Mouse event:', event.button)
        }

        // Can check for focus-specific properties
        if ('relatedTarget' in event) {
          console.log('Focus event:', event.relatedTarget)
        }
      }}
    >
      <Select.Option id="1">Option 1</Select.Option>
      <Select.Option id="2">Option 2</Select.Option>
    </Select>
  )
}

ToMESSKa avatar Dec 03 '25 14:12 ToMESSKa

PR Preview Action v1.6.3 :---: Preview removed because the pull request was closed. 2025-12-23 13:00 UTC

github-actions[bot] avatar Dec 03 '25 14:12 github-actions[bot]