@@ -2156,4 +2156,274 @@ describe('FlexTable', () => {
21562156 expect ( el . data [ 2 ] [ 'name' ] ) . toBe ( 'foo' ) ;
21572157 } ) ;
21582158 } ) ;
2159+
2160+ describe ( 'column format' , ( ) => {
2161+ it ( 'applies string format pattern to cell display' , async ( ) => {
2162+ const el = createElement ( ) ;
2163+ el . columns = [ { key : 'price' , header : 'Price' , type : 'number' , format : '$#,##0.00' } ] ;
2164+ el . data = [ { price : 1234.56 } ] ;
2165+ await el . updateComplete ;
2166+
2167+ const cell = el . shadowRoot ! . querySelector ( '.ft-cell' ) ;
2168+ expect ( cell ! . textContent ?. trim ( ) ) . toMatch ( / ^ \$ [ \d , ] + \. \d { 2 } $ / ) ;
2169+ } ) ;
2170+
2171+ it ( 'applies function format to cell display' , async ( ) => {
2172+ const el = createElement ( ) ;
2173+ el . columns = [ { key : 'val' , header : 'Val' , format : ( v ) => `[${ v } ]` } ] ;
2174+ el . data = [ { val : 42 } ] ;
2175+ await el . updateComplete ;
2176+
2177+ const cell = el . shadowRoot ! . querySelector ( '.ft-cell' ) ;
2178+ expect ( cell ! . textContent ?. trim ( ) ) . toBe ( '[42]' ) ;
2179+ } ) ;
2180+
2181+ it ( 'editor shows raw value, not formatted value' , async ( ) => {
2182+ const el = createElement ( ) ;
2183+ el . columns = [ { key : 'price' , header : 'Price' , type : 'number' , format : '$#,##0.00' } ] ;
2184+ el . data = [ { price : 42 } ] ;
2185+ await el . updateComplete ;
2186+
2187+ ( el as any ) . _editingCell = { row : 0 , col : 0 } ;
2188+ await el . updateComplete ;
2189+
2190+ const input = el . shadowRoot ! . querySelector < HTMLInputElement > ( '.ft-editor' ) ;
2191+ expect ( input ) . toBeTruthy ( ) ;
2192+ expect ( input ! . value ) . toBe ( '42' ) ;
2193+ } ) ;
2194+
2195+ it ( 'clipboard copy uses raw value, not formatted' , async ( ) => {
2196+ const el = createElement ( ) ;
2197+ el . columns = [ { key : 'val' , header : 'Val' , format : ( v ) => `formatted:${ v } ` } ] ;
2198+ el . data = [ { val : 'hello' } ] ;
2199+ await el . updateComplete ;
2200+
2201+ const { copyToClipboard } = await import ( './clipboard/clipboard.js' ) ;
2202+ const text = copyToClipboard ( el . data , el . columns , { startRow : 0 , endRow : 0 , startCol : 0 , endCol : 0 } ) ;
2203+ expect ( text ) . toBe ( 'hello' ) ;
2204+ } ) ;
2205+ } ) ;
2206+
2207+ describe ( 'autocomplete editor' , ( ) => {
2208+ function makeAcEl ( autocomplete : boolean | 'strict' = true ) {
2209+ const el = createElement ( ) ;
2210+ el . columns = [ { key : 'tag' , header : 'Tag' , autocomplete } ] ;
2211+ el . data = [ { tag : 'apple' } , { tag : 'apricot' } , { tag : 'banana' } ] ;
2212+ return el ;
2213+ }
2214+
2215+ it ( 'shows dropdown with matching candidates when typing' , async ( ) => {
2216+ const el = makeAcEl ( ) ;
2217+ await el . updateComplete ;
2218+
2219+ ( el as any ) . _editingCell = { row : 0 , col : 0 } ;
2220+ ( el as any ) . _autocompleteState = { candidates : [ 'apple' , 'apricot' ] , activeIndex : - 1 } ;
2221+ await el . updateComplete ;
2222+
2223+ const dropdown = el . shadowRoot ! . querySelector ( '.ft-autocomplete-dropdown' ) ;
2224+ expect ( dropdown ) . toBeTruthy ( ) ;
2225+ const items = el . shadowRoot ! . querySelectorAll ( '.ft-autocomplete-item' ) ;
2226+ expect ( items . length ) . toBe ( 2 ) ;
2227+ } ) ;
2228+
2229+ it ( 'no dropdown when no candidates' , async ( ) => {
2230+ const el = makeAcEl ( ) ;
2231+ await el . updateComplete ;
2232+
2233+ ( el as any ) . _editingCell = { row : 0 , col : 0 } ;
2234+ ( el as any ) . _autocompleteState = null ;
2235+ await el . updateComplete ;
2236+
2237+ const dropdown = el . shadowRoot ! . querySelector ( '.ft-autocomplete-dropdown' ) ;
2238+ expect ( dropdown ) . toBeNull ( ) ;
2239+ } ) ;
2240+
2241+ it ( '_getAutocompleteCandidates returns unique matching values' , async ( ) => {
2242+ const el = makeAcEl ( ) ;
2243+ await el . updateComplete ;
2244+
2245+ const candidates = ( el as any ) . _getAutocompleteCandidates ( el . columns [ 0 ] , 'ap' ) ;
2246+ expect ( candidates ) . toContain ( 'apple' ) ;
2247+ expect ( candidates ) . toContain ( 'apricot' ) ;
2248+ expect ( candidates ) . not . toContain ( 'banana' ) ;
2249+ } ) ;
2250+
2251+ it ( '_getAutocompleteCandidates with empty text returns all unique values' , async ( ) => {
2252+ const el = makeAcEl ( ) ;
2253+ await el . updateComplete ;
2254+
2255+ const candidates = ( el as any ) . _getAutocompleteCandidates ( el . columns [ 0 ] , '' ) ;
2256+ expect ( candidates ) . toHaveLength ( 3 ) ;
2257+ } ) ;
2258+
2259+ it ( 'ArrowDown increases activeIndex' , async ( ) => {
2260+ const el = makeAcEl ( ) ;
2261+ await el . updateComplete ;
2262+
2263+ ( el as any ) . _editingCell = { row : 0 , col : 0 } ;
2264+ ( el as any ) . _autocompleteState = { candidates : [ 'apple' , 'apricot' ] , activeIndex : - 1 } ;
2265+ await el . updateComplete ;
2266+
2267+ const input = el . shadowRoot ! . querySelector < HTMLInputElement > ( '.ft-editor' ) ;
2268+ input ! . dispatchEvent ( new KeyboardEvent ( 'keydown' , { key : 'ArrowDown' , bubbles : true } ) ) ;
2269+ expect ( ( el as any ) . _autocompleteState . activeIndex ) . toBe ( 0 ) ;
2270+ } ) ;
2271+
2272+ it ( 'strict mode rejects value not in list' , async ( ) => {
2273+ const el = makeAcEl ( 'strict' ) ;
2274+ await el . updateComplete ;
2275+
2276+ ( el as any ) . _editingCell = { row : 0 , col : 0 } ;
2277+ ( el as any ) . _editing . start ( { row : 0 , col : 0 } , 'apple' ) ;
2278+ ( el as any ) . _applyEdit ( 'mango' ) ;
2279+ await el . updateComplete ;
2280+
2281+ // Should not update the value
2282+ expect ( el . data [ 0 ] [ 'tag' ] ) . toBe ( 'apple' ) ;
2283+ } ) ;
2284+
2285+ it ( 'strict mode accepts value in list' , async ( ) => {
2286+ const el = makeAcEl ( 'strict' ) ;
2287+ await el . updateComplete ;
2288+
2289+ ( el as any ) . _editingCell = { row : 0 , col : 0 } ;
2290+ ( el as any ) . _editing . start ( { row : 0 , col : 0 } , 'apple' ) ;
2291+ ( el as any ) . _applyEdit ( 'banana' ) ;
2292+ await el . updateComplete ;
2293+
2294+ expect ( el . data [ 0 ] [ 'tag' ] ) . toBe ( 'banana' ) ;
2295+ } ) ;
2296+ } ) ;
2297+
2298+ describe ( 'column hide/show UI' , ( ) => {
2299+ function makeCols ( ) {
2300+ const el = createElement ( ) ;
2301+ el . columns = [
2302+ { key : 'a' , header : 'A' } ,
2303+ { key : 'b' , header : 'B' } ,
2304+ { key : 'c' , header : 'C' } ,
2305+ ] ;
2306+ el . data = [ { a : 1 , b : 2 , c : 3 } ] ;
2307+ return el ;
2308+ }
2309+
2310+ it ( 'hideColumn hides a column and fires column-visibility-change' , async ( ) => {
2311+ const el = makeCols ( ) ;
2312+ await el . updateComplete ;
2313+
2314+ const events : CustomEvent [ ] = [ ] ;
2315+ el . addEventListener ( 'column-visibility-change' , e => events . push ( e as CustomEvent ) ) ;
2316+
2317+ el . hideColumn ( 'b' ) ;
2318+ await el . updateComplete ;
2319+
2320+ expect ( el . columns . find ( c => c . key === 'b' ) ! . hidden ) . toBe ( true ) ;
2321+ expect ( events ) . toHaveLength ( 1 ) ;
2322+ expect ( events [ 0 ] . detail . key ) . toBe ( 'b' ) ;
2323+ expect ( events [ 0 ] . detail . hidden ) . toBe ( true ) ;
2324+ } ) ;
2325+
2326+ it ( 'showColumn shows a hidden column and fires column-visibility-change' , async ( ) => {
2327+ const el = makeCols ( ) ;
2328+ await el . updateComplete ;
2329+ el . hideColumn ( 'b' ) ;
2330+ await el . updateComplete ;
2331+
2332+ const events : CustomEvent [ ] = [ ] ;
2333+ el . addEventListener ( 'column-visibility-change' , e => events . push ( e as CustomEvent ) ) ;
2334+
2335+ el . showColumn ( 'b' ) ;
2336+ await el . updateComplete ;
2337+
2338+ expect ( el . columns . find ( c => c . key === 'b' ) ! . hidden ) . toBe ( false ) ;
2339+ expect ( events [ 0 ] . detail . hidden ) . toBe ( false ) ;
2340+ } ) ;
2341+
2342+ it ( 'getHiddenColumns returns only hidden columns' , async ( ) => {
2343+ const el = makeCols ( ) ;
2344+ await el . updateComplete ;
2345+ el . hideColumn ( 'b' ) ;
2346+ await el . updateComplete ;
2347+
2348+ const hidden = el . getHiddenColumns ( ) ;
2349+ expect ( hidden ) . toHaveLength ( 1 ) ;
2350+ expect ( hidden [ 0 ] . key ) . toBe ( 'b' ) ;
2351+ } ) ;
2352+
2353+ it ( 'header-context-menu event fires on header right-click' , async ( ) => {
2354+ const el = makeCols ( ) ;
2355+ await el . updateComplete ;
2356+
2357+ const events : CustomEvent [ ] = [ ] ;
2358+ el . addEventListener ( 'header-context-menu' , e => events . push ( e as CustomEvent ) ) ;
2359+
2360+ ( el as any ) . _onHeaderContextMenu (
2361+ { preventDefault : ( ) => { } , clientX : 100 , clientY : 50 } as MouseEvent ,
2362+ el . columns [ 0 ]
2363+ ) ;
2364+
2365+ expect ( events ) . toHaveLength ( 1 ) ;
2366+ expect ( events [ 0 ] . detail . key ) . toBe ( 'a' ) ;
2367+ } ) ;
2368+
2369+ it ( '_renderHeaderContextMenu shows hide-column item' , async ( ) => {
2370+ const el = makeCols ( ) ;
2371+ await el . updateComplete ;
2372+
2373+ ( el as any ) . _headerMenu = { key : 'a' , x : 100 , y : 50 , hiddenNeighbors : [ ] } ;
2374+ await el . updateComplete ;
2375+
2376+ const menu = el . shadowRoot ! . querySelector ( '.ft-header-menu' ) ;
2377+ expect ( menu ) . toBeTruthy ( ) ;
2378+ expect ( menu ! . textContent ) . toContain ( 'Hide' ) ;
2379+ } ) ;
2380+
2381+ it ( 'clicking hide in menu hides the column' , async ( ) => {
2382+ const el = makeCols ( ) ;
2383+ await el . updateComplete ;
2384+
2385+ ( el as any ) . _headerMenu = { key : 'b' , x : 100 , y : 50 , hiddenNeighbors : [ ] } ;
2386+ await el . updateComplete ;
2387+
2388+ const items = el . shadowRoot ! . querySelectorAll < HTMLElement > ( '.ft-header-menu-item' ) ;
2389+ items [ 0 ] . click ( ) ;
2390+ await el . updateComplete ;
2391+
2392+ expect ( el . columns . find ( c => c . key === 'b' ) ! . hidden ) . toBe ( true ) ;
2393+ expect ( ( el as any ) . _headerMenu ) . toBeNull ( ) ;
2394+ } ) ;
2395+ } ) ;
2396+
2397+ describe ( 'conditional formatting' , ( ) => {
2398+ it ( 'applies background style when condition is true' , async ( ) => {
2399+ const el = createElement ( ) ;
2400+ el . columns = [ {
2401+ key : 'score' , header : 'Score' , type : 'number' ,
2402+ conditionalRules : [ { when : ( v ) => ( v as number ) < 60 , style : { background : '#fdd' , color : 'red' } } ]
2403+ } ] ;
2404+ el . data = [ { score : 40 } , { score : 80 } ] ;
2405+ await el . updateComplete ;
2406+
2407+ const cells = el . shadowRoot ! . querySelectorAll < HTMLElement > ( '.ft-cell' ) ;
2408+ expect ( cells [ 0 ] . style . background ) . toBe ( 'rgb(255, 221, 221)' ) ;
2409+ expect ( cells [ 1 ] . style . background ) . toBe ( '' ) ;
2410+ } ) ;
2411+
2412+ it ( 'merges multiple matching rules' , async ( ) => {
2413+ const el = createElement ( ) ;
2414+ el . columns = [ {
2415+ key : 'v' , header : 'V' ,
2416+ conditionalRules : [
2417+ { when : ( v ) => ( v as number ) > 0 , style : { background : 'blue' } } ,
2418+ { when : ( v ) => ( v as number ) > 0 , style : { color : 'white' } } ,
2419+ ]
2420+ } ] ;
2421+ el . data = [ { v : 1 } ] ;
2422+ await el . updateComplete ;
2423+
2424+ const cell = el . shadowRoot ! . querySelector < HTMLElement > ( '.ft-cell' ) ;
2425+ expect ( cell ! . style . background ) . toBe ( 'blue' ) ;
2426+ expect ( cell ! . style . color ) . toBe ( 'white' ) ;
2427+ } ) ;
2428+ } ) ;
21592429} ) ;
0 commit comments