Files
HMDesign/packages/sw/src/components/sw-footer/SWFooterCommandLine.tsx
T

65 lines
3.8 KiB
TypeScript

import { forwardRef, useEffect, useId, useRef, useState, type HTMLAttributes, type InputHTMLAttributes, type ReactNode } from "react"
import "../../theme/sw-footer.css"
export interface SWFooterCommandLineProps extends Omit<HTMLAttributes<HTMLDivElement>, "onSubmit"> {
/** Output lines, oldest first. Collapsed shows the last 3 (Figma 26:10048), expanded scrolls them all (26:10056). */
history: ReactNode[]
/** Input text (controlled). Omit for an uncontrolled input. */
value?: string
onValueChange?: (value: string) => void
/** Enter in the input. The host runs the command and appends to `history`; an uncontrolled input is cleared. */
onSubmit?: (command: string) => void
/** Prompt label before the input. Default `Command:`. */
prompt?: string
/** Expanded (270px) or collapsed (71px). Controlled with `onExpandedChange`. */
expanded?: boolean
defaultExpanded?: boolean
onExpandedChange?: (expanded: boolean) => void
/** Native input attributes (placeholder, autoFocus, …). */
inputProps?: Omit<InputHTMLAttributes<HTMLInputElement>, "value" | "onChange">
/** Accessible names. */
historyLabel?: string
expandLabel?: string
collapseLabel?: string
}
/**
* Command line window (Figma command 26:10048 collapsed / 26:10056 expanded, ico_arrow 26:10023): 566px, 12px text
* with 10% tracking, history above a 23px input well. The host places it (usually above the footer, left).
*/
export const SWFooterCommandLine = forwardRef<HTMLDivElement, SWFooterCommandLineProps>(function SWFooterCommandLine({
history, value, onValueChange, onSubmit, prompt = "Command:", expanded, defaultExpanded = false, onExpandedChange,
inputProps, historyLabel = "명령 기록", expandLabel = "명령 기록 펼치기", collapseLabel = "명령 기록 접기", className, ...rest
}, ref) {
const [ownExpanded, setOwnExpanded] = useState(defaultExpanded)
const [ownValue, setOwnValue] = useState("")
const open = expanded ?? ownExpanded
const text = value ?? ownValue
const inputId = useId()
const historyRef = useRef<HTMLOListElement>(null)
// Newest line in view (collapsed: the last 3 lines; expanded: scrolled to the end).
useEffect(() => { const list = historyRef.current; if (list) list.scrollTop = list.scrollHeight }, [history.length, open])
const toggle = () => { const next = !open; if (expanded === undefined) setOwnExpanded(next); onExpandedChange?.(next) }
return (
<div ref={ref} className={`hm-sw-theme hm-sw-footer-command${className ? ` ${className}` : ""}`} data-expanded={open ? "" : undefined} {...rest}>
<ol ref={historyRef} className="hm-sw-footer-command__history" role="log" aria-label={historyLabel} aria-live="polite">
{/* Collapsed: the last 3 lines (Figma 26:10048); expanded: every line, scrolled to the newest. */}
{(open ? history : history.slice(-3)).map((line, index) => <li key={open ? index : history.length - 3 + index}>{line}</li>)}
</ol>
<button type="button" className="hm-sw-footer-command__toggle" aria-expanded={open} aria-label={open ? collapseLabel : expandLabel} title={open ? collapseLabel : expandLabel} onClick={toggle} />
<div className="hm-sw-footer-command__prompt">
<label className="hm-sw-footer-command__label" htmlFor={inputId}>{prompt}</label>
<input id={inputId} className="hm-sw-footer-command__input" autoComplete="off" spellCheck={false} {...inputProps} value={text}
onChange={event => { if (value === undefined) setOwnValue(event.target.value); onValueChange?.(event.target.value) }}
onKeyDown={event => {
inputProps?.onKeyDown?.(event)
if (event.defaultPrevented || event.key !== "Enter" || event.nativeEvent.isComposing) return
event.preventDefault()
onSubmit?.(text)
if (value === undefined) setOwnValue("")
}} />
</div>
</div>
)
})