( props: AriaTokenFieldProps<T>, state: TokenFieldState, ref: RefObject<HTMLDivElement | null> )
| 97 | * @param state - State for the token field, as returned by `useTokenFieldState`. |
| 98 | */ |
| 99 | export function useTokenField<T extends TokenFieldValue = TokenFieldValue>( |
| 100 | props: AriaTokenFieldProps<T>, |
| 101 | state: TokenFieldState, |
| 102 | ref: RefObject<HTMLDivElement | null> |
| 103 | ): TokenFieldAria { |
| 104 | let { |
| 105 | role = 'textbox', |
| 106 | allowsNewlines: multiline = false, |
| 107 | isReadOnly = false, |
| 108 | isDisabled = false, |
| 109 | 'aria-details': ariaDetails |
| 110 | } = props; |
| 111 | |
| 112 | let {value} = state; |
| 113 | let {locale} = useLocale(); |
| 114 | let graphemeSegmenter = useMemo( |
| 115 | () => new Intl.Segmenter(locale, {granularity: 'grapheme'}), |
| 116 | [locale] |
| 117 | ); |
| 118 | let wordSegmenter = useMemo(() => new Intl.Segmenter(locale, {granularity: 'word'}), [locale]); |
| 119 | |
| 120 | let dropPosition = useRef<Position | null>(null); |
| 121 | let transferredData = useRef<TokenFieldSegment[] | null>(null); |
| 122 | |
| 123 | let nextValue = useRef<TokenFieldValue | null>(null); |
| 124 | let apply = (fn: (value: TokenFieldValue) => TokenFieldValue) => { |
| 125 | state.setValue(value => { |
| 126 | let newValue = fn(value); |
| 127 | nextValue.current = newValue; |
| 128 | return newValue; |
| 129 | }); |
| 130 | }; |
| 131 | |
| 132 | // Composition events are not cancelable. The browser will mutate the DOM, making it out of sync with React. |
| 133 | // To account for this, we prevent React from re-rendering during composition, and track DOM mutations performed |
| 134 | // by the browser. When composition ends, we revert the DOM to its original state, and re-render with React. |
| 135 | // Mutating the DOM in any way during composition breaks the IME, causing composition to end unexpectedly. |
| 136 | // During composition, we still emit updates via onChange to ensure that things like autocomplete work, |
| 137 | // but we don't actually re-render to the DOM unless the value changes from what we expect (e.g. inserting a completion). |
| 138 | let mutationTracker = useMutationTracker(ref); |
| 139 | let startComposition = useCallback(() => { |
| 140 | mutationTracker.start(); |
| 141 | state.setComposing(true); |
| 142 | }, [state, mutationTracker]); |
| 143 | let stopComposition = useCallback(() => { |
| 144 | mutationTracker.stop(); |
| 145 | state.setComposing(false); |
| 146 | }, [state, mutationTracker]); |
| 147 | |
| 148 | useEvent(ref, 'compositionstart', () => { |
| 149 | startComposition(); |
| 150 | |
| 151 | let range = window.getSelection()?.getRangeAt(0); |
| 152 | if (range) { |
| 153 | let [start, end] = rangeToPositions(ref.current!, range); |
| 154 | |
| 155 | // Normalize the range to ensure it is not inside a token, otherwise the browser |
| 156 | // will attempt to insert the composed text into the token instead of replacing it. |
no test coverage detected