@prairielearn/ui 3.2.1 → 3.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/dist/components/CategoricalColumnFilter.js.map +1 -1
- package/dist/components/ColumnManager.js.map +1 -1
- package/dist/components/ComboBox.d.ts +1 -1
- package/dist/components/ComboBox.d.ts.map +1 -1
- package/dist/components/ComboBox.js.map +1 -1
- package/dist/components/FilterDropdown.js.map +1 -1
- package/dist/components/MultiSelectColumnFilter.js.map +1 -1
- package/dist/components/NumericInputColumnFilter.js.map +1 -1
- package/dist/components/NumericInputColumnFilter.test.js.map +1 -1
- package/dist/components/OverlayTrigger.js.map +1 -1
- package/dist/components/PresetFilterDropdown.js.map +1 -1
- package/dist/components/TanstackTable.d.ts.map +1 -1
- package/dist/components/TanstackTable.js +1 -0
- package/dist/components/TanstackTable.js.map +1 -1
- package/dist/components/TanstackTableDownloadButton.js.map +1 -1
- package/dist/components/TanstackTableHeaderCell.js.map +1 -1
- package/dist/components/nuqs.js.map +1 -1
- package/dist/components/nuqs.test.js.map +1 -1
- package/dist/components/useAutoSizeColumns.js.map +1 -1
- package/dist/components/useShiftClickCheckbox.js.map +1 -1
- package/dist/hooks/use-modal-state.js.map +1 -1
- package/package.json +4 -4
- package/src/components/ComboBox.tsx +4 -1
- package/src/components/TanstackTable.tsx +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CategoricalColumnFilter.js","sourceRoot":"","sources":["../../src/components/CategoricalColumnFilter.tsx"],"names":[],"mappings":"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","sourcesContent":["import type { Column } from '@tanstack/react-table';\nimport clsx from 'clsx';\nimport { type ReactNode, useMemo, useState } from 'react';\nimport Dropdown from 'react-bootstrap/Dropdown';\n\nfunction computeSelected<TValue extends string>(\n allStatusValues: TValue[] | readonly TValue[],\n mode: 'include' | 'exclude',\n selected: Set<TValue>,\n): Set<TValue> {\n if (mode === 'include') {\n return selected;\n }\n return new Set(allStatusValues.filter((s) => !selected.has(s)));\n}\n\nfunction defaultRenderValueLabel({ value }: { value: string }) {\n return <span className=\"text-nowrap\">{value}</span>;\n}\n\n/**\n * A component that allows the user to filter a categorical column.\n * The filter mode always defaults to \"include\".\n *\n * The filter options (`allColumnValues`) are strings (or string subtypes like\n * enums). The column's `filterFn` is responsible for mapping these string\n * values to the actual column data (e.g., mapping \"Unassigned\" to `null`).\n *\n * @param params\n * @param params.column - The TanStack Table column object\n * @param params.allColumnValues - The string values to display as filter options\n * @param params.renderValueLabel - A function that renders the label for a value\n */\nexport function CategoricalColumnFilter<TData, TValue extends string = string>({\n column,\n allColumnValues,\n renderValueLabel = defaultRenderValueLabel,\n}: {\n column: Column<TData, unknown>;\n allColumnValues: TValue[] | readonly TValue[];\n renderValueLabel?: (props: { value: TValue; isSelected: boolean }) => ReactNode;\n}) {\n const [mode, setMode] = useState<'include' | 'exclude'>('include');\n\n const columnId = column.id;\n\n const label =\n column.columnDef.meta?.label ??\n (typeof column.columnDef.header === 'string' ? column.columnDef.header : column.id);\n\n const columnValuesFilter = column.getFilterValue() as TValue[] | undefined;\n\n const selected = useMemo(() => {\n return computeSelected(allColumnValues, mode, new Set(columnValuesFilter));\n }, [mode, allColumnValues, columnValuesFilter]);\n\n const apply = (newMode: 'include' | 'exclude', newSelected: Set<TValue>) => {\n const selected = computeSelected(allColumnValues, newMode, newSelected);\n setMode(newMode);\n const newValue = Array.from(selected);\n column.setFilterValue(newValue);\n };\n\n const toggleSelected = (value: TValue) => {\n const set = new Set(selected);\n if (set.has(value)) {\n set.delete(value);\n } else {\n set.add(value);\n }\n apply(mode, set);\n };\n\n return (\n <Dropdown align=\"end\">\n <Dropdown.Toggle\n variant=\"link\"\n className=\"text-muted p-0\"\n id={`filter-${columnId}`}\n aria-label={`Filter ${label.toLowerCase()}`}\n title={`Filter ${label.toLowerCase()}`}\n >\n <i\n className={clsx(\n 'bi',\n selected.size > 0 ? ['bi-funnel-fill', 'text-primary'] : 'bi-funnel',\n )}\n aria-hidden=\"true\"\n />\n </Dropdown.Toggle>\n <Dropdown.Menu className=\"p-0\">\n <div className=\"p-3 pb-0\">\n <div className=\"d-flex align-items-center justify-content-between mb-2\">\n <div className=\"fw-semibold text-nowrap\">{label}</div>\n <button\n type=\"button\"\n className={clsx('btn btn-link btn-sm text-decoration-none', {\n // Hide the clear button if no filters are applied.\n // Use `visibility` instead of conditional rendering to avoid layout shift.\n invisible: selected.size === 0 && mode === 'include',\n })}\n onClick={() => apply('include', new Set<TValue>())}\n >\n Clear\n </button>\n </div>\n\n <div className=\"btn-group btn-group-sm w-100 mb-2\">\n <input\n type=\"radio\"\n className=\"btn-check\"\n name={`filter-${columnId}-options`}\n id={`filter-${columnId}-include`}\n autoComplete=\"off\"\n checked={mode === 'include'}\n onChange={() => apply('include', selected)}\n />\n <label className=\"btn btn-outline-primary\" htmlFor={`filter-${columnId}-include`}>\n <span className=\"text-nowrap\">\n {mode === 'include' && <i className=\"bi bi-check-lg me-1\" aria-hidden=\"true\" />}\n Include\n </span>\n </label>\n\n <input\n type=\"radio\"\n className=\"btn-check\"\n name={`filter-${columnId}-options`}\n id={`filter-${columnId}-exclude`}\n autoComplete=\"off\"\n checked={mode === 'exclude'}\n onChange={() => apply('exclude', selected)}\n />\n <label className=\"btn btn-outline-primary\" htmlFor={`filter-${columnId}-exclude`}>\n <span className=\"text-nowrap\">\n {mode === 'exclude' && <i className=\"bi bi-check-lg me-1\" aria-hidden=\"true\" />}\n Exclude\n </span>\n </label>\n </div>\n </div>\n\n <div\n className=\"list-group list-group-flush\"\n style={\n {\n // This is needed to prevent the last item's background from covering\n // the dropdown's border radius.\n '--bs-list-group-bg': 'transparent',\n } as React.CSSProperties\n }\n >\n {allColumnValues.map((value) => {\n const isSelected = selected.has(value);\n return (\n <div key={value} className=\"list-group-item d-flex align-items-center gap-3\">\n <div className=\"form-check\">\n <input\n className=\"form-check-input\"\n type=\"checkbox\"\n checked={isSelected}\n id={`${columnId}-${value}`}\n onChange={() => toggleSelected(value)}\n />\n <label className=\"form-check-label fw-normal\" htmlFor={`${columnId}-${value}`}>\n {renderValueLabel({\n value,\n isSelected,\n })}\n </label>\n </div>\n </div>\n );\n })}\n </div>\n </Dropdown.Menu>\n </Dropdown>\n );\n}\n"]}
|
|
1
|
+
{"version":3,"file":"CategoricalColumnFilter.js","sourceRoot":"","sources":["../../src/components/CategoricalColumnFilter.tsx"],"names":[],"mappings":"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","sourcesContent":["import type { Column } from '@tanstack/react-table';\nimport clsx from 'clsx';\nimport { type ReactNode, useMemo, useState } from 'react';\nimport Dropdown from 'react-bootstrap/Dropdown';\n\nfunction computeSelected<TValue extends string>(\n allStatusValues: TValue[] | readonly TValue[],\n mode: 'include' | 'exclude',\n selected: Set<TValue>,\n): Set<TValue> {\n if (mode === 'include') {\n return selected;\n }\n return new Set(allStatusValues.filter((s) => !selected.has(s)));\n}\n\nfunction defaultRenderValueLabel({ value }: { value: string }) {\n return <span className=\"text-nowrap\">{value}</span>;\n}\n\n/**\n * A component that allows the user to filter a categorical column.\n * The filter mode always defaults to \"include\".\n *\n * The filter options (`allColumnValues`) are strings (or string subtypes like\n * enums). The column's `filterFn` is responsible for mapping these string\n * values to the actual column data (e.g., mapping \"Unassigned\" to `null`).\n *\n * @param params\n * @param params.column - The TanStack Table column object\n * @param params.allColumnValues - The string values to display as filter options\n * @param params.renderValueLabel - A function that renders the label for a value\n */\nexport function CategoricalColumnFilter<TData, TValue extends string = string>({\n column,\n allColumnValues,\n renderValueLabel = defaultRenderValueLabel,\n}: {\n column: Column<TData, unknown>;\n allColumnValues: TValue[] | readonly TValue[];\n renderValueLabel?: (props: { value: TValue; isSelected: boolean }) => ReactNode;\n}) {\n const [mode, setMode] = useState<'include' | 'exclude'>('include');\n\n const columnId = column.id;\n\n const label =\n column.columnDef.meta?.label ??\n (typeof column.columnDef.header === 'string' ? column.columnDef.header : column.id);\n\n const columnValuesFilter = column.getFilterValue() as TValue[] | undefined;\n\n const selected = useMemo(() => {\n return computeSelected(allColumnValues, mode, new Set(columnValuesFilter));\n }, [mode, allColumnValues, columnValuesFilter]);\n\n const apply = (newMode: 'include' | 'exclude', newSelected: Set<TValue>) => {\n const selected = computeSelected(allColumnValues, newMode, newSelected);\n setMode(newMode);\n const newValue = Array.from(selected);\n column.setFilterValue(newValue);\n };\n\n const toggleSelected = (value: TValue) => {\n const set = new Set(selected);\n if (set.has(value)) {\n set.delete(value);\n } else {\n set.add(value);\n }\n apply(mode, set);\n };\n\n return (\n <Dropdown align=\"end\">\n <Dropdown.Toggle\n variant=\"link\"\n className=\"text-muted p-0\"\n id={`filter-${columnId}`}\n aria-label={`Filter ${label.toLowerCase()}`}\n title={`Filter ${label.toLowerCase()}`}\n >\n <i\n className={clsx(\n 'bi',\n selected.size > 0 ? ['bi-funnel-fill', 'text-primary'] : 'bi-funnel',\n )}\n aria-hidden=\"true\"\n />\n </Dropdown.Toggle>\n <Dropdown.Menu className=\"p-0\">\n <div className=\"p-3 pb-0\">\n <div className=\"d-flex align-items-center justify-content-between mb-2\">\n <div className=\"fw-semibold text-nowrap\">{label}</div>\n <button\n type=\"button\"\n className={clsx('btn btn-link btn-sm text-decoration-none', {\n // Hide the clear button if no filters are applied.\n // Use `visibility` instead of conditional rendering to avoid layout shift.\n invisible: selected.size === 0 && mode === 'include',\n })}\n onClick={() => apply('include', new Set<TValue>())}\n >\n Clear\n </button>\n </div>\n\n <div className=\"btn-group btn-group-sm w-100 mb-2\">\n <input\n type=\"radio\"\n className=\"btn-check\"\n name={`filter-${columnId}-options`}\n id={`filter-${columnId}-include`}\n autoComplete=\"off\"\n checked={mode === 'include'}\n onChange={() => apply('include', selected)}\n />\n <label className=\"btn btn-outline-primary\" htmlFor={`filter-${columnId}-include`}>\n <span className=\"text-nowrap\">\n {mode === 'include' && <i className=\"bi bi-check-lg me-1\" aria-hidden=\"true\" />}\n Include\n </span>\n </label>\n\n <input\n type=\"radio\"\n className=\"btn-check\"\n name={`filter-${columnId}-options`}\n id={`filter-${columnId}-exclude`}\n autoComplete=\"off\"\n checked={mode === 'exclude'}\n onChange={() => apply('exclude', selected)}\n />\n <label className=\"btn btn-outline-primary\" htmlFor={`filter-${columnId}-exclude`}>\n <span className=\"text-nowrap\">\n {mode === 'exclude' && <i className=\"bi bi-check-lg me-1\" aria-hidden=\"true\" />}\n Exclude\n </span>\n </label>\n </div>\n </div>\n\n <div\n className=\"list-group list-group-flush\"\n style={\n {\n // This is needed to prevent the last item's background from covering\n // the dropdown's border radius.\n '--bs-list-group-bg': 'transparent',\n } as React.CSSProperties\n }\n >\n {allColumnValues.map((value) => {\n const isSelected = selected.has(value);\n return (\n <div key={value} className=\"list-group-item d-flex align-items-center gap-3\">\n <div className=\"form-check\">\n <input\n className=\"form-check-input\"\n type=\"checkbox\"\n checked={isSelected}\n id={`${columnId}-${value}`}\n onChange={() => toggleSelected(value)}\n />\n <label className=\"form-check-label fw-normal\" htmlFor={`${columnId}-${value}`}>\n {renderValueLabel({\n value,\n isSelected,\n })}\n </label>\n </div>\n </div>\n );\n })}\n </div>\n </Dropdown.Menu>\n </Dropdown>\n );\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ColumnManager.js","sourceRoot":"","sources":["../../src/components/ColumnManager.tsx"],"names":[],"mappings":";AAAA,OAAO,EAA2B,MAAM,uBAAuB,CAAC;AAChE,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAkB,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AACpE,OAAO,MAAM,MAAM,wBAAwB,CAAC;AAC5C,OAAO,QAAQ,MAAM,0BAA0B,CAAC;AAShD,SAAS,cAAc,CAAe,EACpC,MAAM,EACN,iBAAiB,GAAG,KAAK,EACzB,WAAW,EACX,SAAS,GACyB,EAAE;IACpC,IAAI,CAAC,MAAM,CAAC,UAAU,EAAE;QAAE,OAAO,IAAI,CAAC;IAEtC,0EAA0E;IAC1E,MAAM,MAAM,GACV,MAAM,CAAC,SAAS,CAAC,IAAI,EAAE,KAAK;QAC5B,CAAC,OAAO,MAAM,CAAC,SAAS,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IAEtF,OAAO,CACL,eAEE,SAAS,EAAE,IAAI,CAAC,6DAA6D,EAAE,SAAS,CAAC;YAEzF,iBAAO,SAAS,EAAC,2DAA2D;oBAC1E,gBACE,IAAI,EAAC,UAAU,EACf,SAAS,EAAC,kBAAkB,EAC5B,OAAO,EAAE,MAAM,CAAC,YAAY,EAAE,EAC9B,QAAQ,EAAE,CAAC,MAAM,CAAC,UAAU,EAAE,gBAClB,MAAM,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC,SAAS,MAAM,UAAU,CAAC,CAAC,CAAC,SAAS,MAAM,UAAU,sBACvE,GAAG,MAAM,CAAC,EAAE,QAAQ,EACtC,QAAQ,EAAE,MAAM,CAAC,0BAA0B,EAAE,GAC7C,EACF,eAAM,SAAS,EAAC,uBAAuB,EAAC,EAAE,EAAE,GAAG,MAAM,CAAC,EAAE,QAAQ,YAC7D,MAAM,GACF;oBACD,EACR,iBACE,IAAI,EAAC,QAAQ;gBACb,0EAA0E;gBAC1E,gFAAgF;gBAChF,EAAE,EAAE,GAAG,MAAM,CAAC,EAAE,MAAM,EACtB,SAAS,EAAE,IAAI,CACb,2BAA2B,EAC3B,CAAC,CAAC,MAAM,CAAC,SAAS,EAAE,IAAI,CAAC,iBAAiB,CAAC,IAAI,WAAW,CAC3D,gBAEC,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,aAAa,MAAM,UAAU,CAAC,CAAC,CAAC,WAAW,MAAM,WAAW,EAErF,KAAK,EAAE,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,iBAAiB,CAAC,CAAC,CAAC,eAAe,oBAClD,SAAS,EACxB,OAAO,EAAE,GAAG,EAAE,CAAC,WAAW,CAAC,MAAM,CAAC,EAAE,CAAC,YAErC,YAAG,SAAS,EAAE,MAAM,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,EAAE,iBAAc,MAAM,GAAG,GAC/E;aAlCJ,MAAM,CAAC,EAAE,CAmCV,CACP,CAAC;AAAA,CACH;AAED,SAAS,eAAe,CAAe,EACrC,MAAM,EACN,KAAK,EACL,WAAW,EACX,sBAAsB,GAMvB,EAAE;IACD,MAAM,CAAC,UAAU,EAAE,aAAa,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IACpD,MAAM,WAAW,GAAG,MAAM,CAAmB,IAAI,CAAC,CAAC;IAEnD,MAAM,WAAW,GAAG,MAAM,CAAC,cAAc,EAAE,CAAC;IAC5C,MAAM,kBAAkB,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,YAAY,EAAE,CAAC,CAAC;IACvE,MAAM,YAAY,GAAG,kBAAkB,CAAC,MAAM,KAAK,WAAW,CAAC,MAAM,CAAC;IACtE,MAAM,aAAa,GAAG,kBAAkB,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,YAAY,CAAC;IAErE,mFAAmF;IACnF,SAAS,CAAC,GAAG,EAAE,CAAC;QACd,IAAI,WAAW,CAAC,OAAO,EAAE,CAAC;YACxB,WAAW,CAAC,OAAO,CAAC,aAAa,GAAG,aAAa,CAAC;QACpD,CAAC;IAAA,CACF,EAAE,CAAC,aAAa,CAAC,CAAC,CAAC;IAEpB,MAAM,sBAAsB,GAAG,CAAC,CAAsC,EAAE,EAAE,CAAC;QACzE,CAAC,CAAC,cAAc,EAAE,CAAC;QACnB,CAAC,CAAC,eAAe,EAAE,CAAC;QACpB,MAAM,gBAAgB,GAAG,CAAC,YAAY,CAAC;QACvC,oDAAoD;QACpD,kFAAkF;QAClF,8DAA8D;QAC9D,KAAK,CAAC,mBAAmB,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC;YACjC,MAAM,aAAa,GAAG,EAAE,GAAG,GAAG,EAAE,CAAC;YACjC,WAAW,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC;gBAC3B,IAAI,GAAG,CAAC,UAAU,EAAE,EAAE,CAAC;oBACrB,aAAa,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,gBAAgB,CAAC;gBAC3C,CAAC;YAAA,CACF,CAAC,CAAC;YACH,OAAO,aAAa,CAAC;QAAA,CACtB,CAAC,CAAC;IAAA,CACJ,CAAC;IAEF,0EAA0E;IAC1E,MAAM,MAAM,GACV,MAAM,CAAC,SAAS,CAAC,IAAI,EAAE,KAAK;QAC5B,CAAC,OAAO,MAAM,CAAC,SAAS,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IAEtF,OAAO,CACL,eAAK,SAAS,EAAC,oBAAoB;YACjC,cAAK,SAAS,EAAC,6DAA6D,YAC1E,eAAK,SAAS,EAAC,uCAAuC;wBACpD,gBACE,GAAG,EAAE,WAAW,EAChB,IAAI,EAAC,UAAU,EACf,SAAS,EAAC,gCAAgC,EAC1C,OAAO,EAAE,YAAY,gBACT,gCAAgC,MAAM,GAAG,EACrD,QAAQ,EAAE,sBAAsB,GAChC,EACF,kBACE,IAAI,EAAC,QAAQ,EACb,SAAS,EAAC,gIAAgI,mBAC3H,UAAU,EACzB,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC;gCACd,CAAC,CAAC,eAAe,EAAE,CAAC;gCACpB,aAAa,CAAC,CAAC,UAAU,CAAC,CAAC;4BAAA,CAC5B;gCAED,eAAM,SAAS,EAAC,uBAAuB,YAAE,MAAM,GAAQ,EACvD,YACE,SAAS,EAAE,IAAI,CACb,oBAAoB,EACpB,UAAU,CAAC,CAAC,CAAC,iBAAiB,CAAC,CAAC,CAAC,kBAAkB,CACpD,iBACW,MAAM,GAClB;gCACK;wBACL,GACF,EACL,UAAU,IAAI,CACb,cAAK,SAAS,EAAC,6BAA6B,YACzC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,CAChC,KAAC,UAAU,IAET,MAAM,EAAE,QAAQ,EAChB,KAAK,EAAE,KAAK,EACZ,sBAAsB,EAAE,sBAAsB,EAC9C,WAAW,EAAE,WAAW,IAJnB,QAAQ,CAAC,EAAE,CAKhB,CACH,CAAC,GACE,CACP,IACG,CACP,CAAC;AAAA,CACH;AAED,SAAS,UAAU,CAAe,EAChC,MAAM,EACN,KAAK,EACL,WAAW,EACX,sBAAsB,GAMvB,EAAE;IACD,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC9B,OAAO,CACL,KAAC,eAAe,IACd,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,KAAK,EACZ,sBAAsB,EAAE,sBAAsB,EAC9C,WAAW,EAAE,WAAW,GACxB,CACH,CAAC;IACJ,CAAC;IACD,OAAO,CACL,KAAC,cAAc,IACb,MAAM,EAAE,MAAM,EACd,iBAAiB,EAAE,sBAAsB,CAAC,MAAM,CAAC,EAAE,CAAC,EACpD,WAAW,EAAE,WAAW,GACxB,CACH,CAAC;AAAA,CACH;AAOD;;;GAGG;AACH,SAAS,aAAa,CAAI,GAAQ,EAAE,SAA+C,EAAU;IAC3F,KAAK,IAAI,CAAC,GAAG,GAAG,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QACzC,IAAI,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;YACzB,OAAO,CAAC,CAAC;QACX,CAAC;IACH,CAAC;IACD,OAAO,CAAC,CAAC,CAAC;AAAA,CACX;AAED,MAAM,UAAU,aAAa,CAAe,EAC1C,KAAK,EACL,UAAU,GACuB,EAAE;IACnC,MAAM,CAAC,eAAe,EAAE,kBAAkB,CAAC,GAAG,QAAQ,CAAgB,IAAI,CAAC,CAAC;IAC5E,MAAM,CAAC,YAAY,EAAE,eAAe,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IACxD,MAAM,OAAO,GAAG,MAAM,CAAiB,IAAI,CAAC,CAAC;IAC7C,MAAM,eAAe,GAAG,CAAC,QAAgB,EAAE,EAAE,CAAC;QAC5C,MAAM,WAAW,GAAG,KAAK,CAAC,QAAQ,EAAE,CAAC,aAAa,CAAC,IAAI,IAAI,EAAE,CAAC;QAC9D,MAAM,QAAQ,GAAG,WAAW,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QAChD,MAAM,cAAc,GAAG,KAAK,CAAC,iBAAiB,EAAE,CAAC;QACjD,MAAM,kBAAkB,GAAG,cAAc,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,QAAQ,CAAC,CAAC;QAC9E,IAAI,OAAiB,CAAC;QACtB,IAAI,QAAQ,EAAE,CAAC;YACb,uDAAuD;YACvD,4FAA4F;YAC5F,MAAM,yBAAyB,GAAG,aAAa,CAC7C,cAAc,EACd,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,EAAE,IAAI,KAAK,GAAG,kBAAkB,CAC3D,CAAC;YACF,OAAO,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC,EAAE,yBAAyB,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QACpF,CAAC;aAAM,CAAC;YACN,qDAAqD;YACrD,MAAM,WAAW,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC,EAAE,kBAAkB,GAAG,CAAC,CAAC,CAAC;YACpE,OAAO,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QACzC,CAAC;QACD,KAAK,CAAC,gBAAgB,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC;QACrD,kBAAkB,CAAC,GAAG,QAAQ,MAAM,CAAC,CAAC;IAAA,CACvC,CAAC;IAEF,MAAM,mBAAmB,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC,gBAAgB,CAAC,CAAC,IAAI,CAChF,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC;QAChB,OAAO,KAAK,KAAK,KAAK,CAAC,YAAY,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAAC;IAAA,CAC3D,CACF,CAAC;IAEF,MAAM,cAAc,GAAG,KAAK,CAAC,YAAY,CAAC,aAAa,CAAC,IAAI,IAAI,EAAE,CAAC;IACnE,MAAM,cAAc,GAAG,KAAK,CAAC,QAAQ,EAAE,CAAC,aAAa,CAAC,IAAI,IAAI,EAAE,CAAC;IACjE,MAAM,gBAAgB,GACpB,cAAc,CAAC,MAAM,KAAK,cAAc,CAAC,MAAM;QAC/C,cAAc,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,cAAc,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC;IAC5D,MAAM,eAAe,GAAG,mBAAmB,IAAI,gBAAgB,CAAC;IAEhE,MAAM,cAAc,GAAG,KAAK,CAAC,iBAAiB,EAAE,CAAC;IACjD,MAAM,iBAAiB,GAAG,cAAc,CAAC,MAAM,CAC7C,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC,WAAW,EAAE,KAAK,MAAM,CACpD,CAAC;IACF,2FAA2F;IAC3F,MAAM,uBAAuB,GAAG,cAAc,CAAC,IAAI,CACjD,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC,WAAW,EAAE,KAAK,MAAM,CACpD,CAAC;IAEF,+EAA+E;IAC/E,wCAAwC;IACxC,2CAA2C;IAC3C,qDAAqD;IACrD,sDAAsD;IACtD,MAAM,sBAAsB,GAAG,CAAC,QAAgB,EAAE,EAAE,CAAC;QACnD,MAAM,MAAM,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,QAAQ,CAAC,CAAC;QAC7D,IAAI,CAAC,MAAM;YAAE,OAAO,KAAK,CAAC;QAE1B,sCAAsC;QACtC,IAAI,MAAM,CAAC,MAAM;YAAE,OAAO,KAAK,CAAC;QAEhC,mFAAmF;QACnF,MAAM,SAAS,GAAG,cAAc,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,QAAQ,CAAC,CAAC;QACrE,MAAM,kBAAkB,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC,EAAE,SAAS,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;QAExF,IAAI,MAAM,CAAC,WAAW,EAAE,KAAK,MAAM,EAAE,CAAC;YACpC,mDAAmD;YACnD,OAAO,QAAQ,KAAK,iBAAiB,CAAC,iBAAiB,CAAC,MAAM,GAAG,CAAC,CAAC,EAAE,EAAE,CAAC;QAC1E,CAAC;aAAM,CAAC;YACN,yDAAyD;YACzD,IAAI,kBAAkB;gBAAE,OAAO,KAAK,CAAC;YACrC,oDAAoD;YACpD,OAAO,QAAQ,KAAK,uBAAuB,EAAE,EAAE,CAAC;QAClD,CAAC;IAAA,CACF,CAAC;IAEF,kFAAkF;IAClF,uEAAuE;IACvE,MAAM,mBAAmB,GAAG,KAAK,CAAC,aAAa,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;QAC9D,IAAI,CAAC,CAAC,KAAK,KAAK,CAAC;YAAE,OAAO,KAAK,CAAC;QAChC,4EAA4E;QAC5E,MAAM,QAAQ,GAAG,CAAC,CAAC,cAAc,EAAE,CAAC;QACpC,OAAO,CACL,QAAQ,CAAC,MAAM,GAAG,CAAC;YACnB,QAAQ,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,MAAM,IAAI,CAAC,CAAC,UAAU,EAAE,CAAC,CAC1E,CAAC;IAAA,CACH,CAAC,CAAC;IAEH,SAAS,CAAC,GAAG,EAAE,CAAC;QACd,8EAA8E;QAC9E,qFAAqF;QAErF,+EAA+E;QAC/E,IAAI,eAAe,EAAE,CAAC;YACpB,QAAQ,CAAC,cAAc,CAAC,eAAe,CAAC,EAAE,KAAK,EAAE,CAAC;QACpD,CAAC;IAAA,CACF,EAAE,CAAC,eAAe,CAAC,CAAC,CAAC;IAEtB,OAAO,CACL,MAAC,QAAQ,IACP,GAAG,EAAE,OAAO,EACZ,SAAS,EAAC,SAAS,EACnB,IAAI,EAAE,YAAY,EAClB,QAAQ,EAAE,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC,eAAe,CAAC,MAAM,CAAC,EACpD,MAAM,EAAE,CAAC,CAAmB,EAAE,EAAE,CAAC;YAC/B,+FAA+F;YAC/F,gDAAgD;YAChD,IAAI,OAAO,CAAC,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,aAAa,CAAC,EAAE,CAAC;gBAClE,eAAe,CAAC,KAAK,CAAC,CAAC;YACzB,CAAC;QAAA,CACF;YAED,MAAC,QAAQ,CAAC,MAAM;YACd,iFAAiF;YACjF,2DAA2D;;gBAD3D,iFAAiF;gBACjF,2DAA2D;gBAC3D,EAAE,EAAC,gBAAgB,EACnB,OAAO,EAAC,gBAAgB;oBAExB,YAAG,SAAS,EAAC,sBAAsB,iBAAa,MAAM,GAAG;6BAAM,GAAG,IAClD,EAClB,MAAC,QAAQ,CAAC,IAAI,IAAC,KAAK,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,aAC3D,UAAU,IAAI,CACb,8BACG,UAAU,EACX,KAAC,QAAQ,CAAC,OAAO,KAAG;4BACnB,CACJ,EACA,iBAAiB,CAAC,MAAM,GAAG,CAAC,IAAI,CAC/B;4BACE,cAAK,SAAS,EAAC,4BAA4B,EAAC,IAAI,EAAC,cAAc,+BAEzD,EACN,cAAK,IAAI,EAAC,OAAO,YAEd,iBAAiB,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC;oCACxC,OAAO,CACL,KAAC,cAAc,IAEb,MAAM,EAAE,MAAM,EACd,iBAAiB,EAAE,KAAK,KAAK,iBAAiB,CAAC,MAAM,GAAG,CAAC,EACzD,WAAW,EAAE,eAAe,IAHvB,MAAM,CAAC,EAAE,CAId,CACH,CAAC;gCAAA,CACH,CAAC,GACE,EACN,KAAC,QAAQ,CAAC,OAAO,KAAG;4BACnB,CACJ,EACA,mBAAmB,CAAC,MAAM,GAAG,CAAC,IAAI,CACjC;4BACE,cAAK,IAAI,EAAC,OAAO,YACd,mBAAmB,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC;oCACnC,OAAO,CACL,KAAC,UAAU,IAET,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,KAAK,EACZ,sBAAsB,EAAE,sBAAsB,EAC9C,WAAW,EAAE,eAAe,IAJvB,MAAM,CAAC,EAAE,CAKd,CACH,CAAC;gCAAA,CACH,CAAC,GACE,EACL,eAAe,IAAI,KAAC,QAAQ,CAAC,OAAO,KAAG,IACvC,CACJ,EACA,eAAe,IAAI,CAClB,cAAK,SAAS,EAAC,WAAW,YACxB,MAAC,MAAM,IACL,OAAO,EAAC,WAAW,EACnB,IAAI,EAAC,IAAI,EACT,SAAS,EAAC,OAAO,gBACN,qDAAqD,EAChE,OAAO,EAAE,GAAG,EAAE,CAAC;gCACb,KAAK,CAAC,qBAAqB,EAAE,CAAC;gCAC9B,KAAK,CAAC,kBAAkB,EAAE,CAAC;gCAC3B,2DAA2D;gCAC3D,kBAAkB,CAAC,gBAAgB,CAAC,CAAC;4BAAA,CACtC;gCAED,YAAG,SAAS,EAAC,mCAAmC,iBAAa,MAAM,GAAG;gDAE/D,GACL,CACP,IACa;YACP,CACZ,CAAC;AAAA,CACH","sourcesContent":["import { type Column, type Table } from '@tanstack/react-table';\nimport clsx from 'clsx';\nimport { type ReactNode, useEffect, useRef, useState } from 'react';\nimport Button from 'react-bootstrap/Button';\nimport Dropdown from 'react-bootstrap/Dropdown';\n\ninterface ColumnMenuItemProps<RowDataModel> {\n column: Column<RowDataModel>;\n onPinningBoundary: boolean;\n onTogglePin: (columnId: string) => void;\n className?: string;\n}\n\nfunction ColumnLeafItem<RowDataModel>({\n column,\n onPinningBoundary = false,\n onTogglePin,\n className,\n}: ColumnMenuItemProps<RowDataModel>) {\n if (!column.getCanHide()) return null;\n\n // Use meta.label if available, otherwise fall back to header or column.id\n const header =\n column.columnDef.meta?.label ??\n (typeof column.columnDef.header === 'string' ? column.columnDef.header : column.id);\n\n return (\n <div\n key={column.id}\n className={clsx('px-2 py-1 d-flex align-items-center justify-content-between', className)}\n >\n <label className=\"form-check me-auto text-nowrap d-flex align-items-stretch\">\n <input\n type=\"checkbox\"\n className=\"form-check-input\"\n checked={column.getIsVisible()}\n disabled={!column.getCanHide()}\n aria-label={column.getIsVisible() ? `Hide '${header}' column` : `Show '${header}' column`}\n aria-describedby={`${column.id}-label`}\n onChange={column.getToggleVisibilityHandler()}\n />\n <span className=\"form-check-label ms-2\" id={`${column.id}-label`}>\n {header}\n </span>\n </label>\n <button\n type=\"button\"\n // Since the HTML changes, but we want to refocus the pin button, we track\n // the active pin button and refocuses it when the column manager is rerendered.\n id={`${column.id}-pin`}\n className={clsx(\n 'btn btn-sm btn-ghost ms-2',\n (!column.getCanPin() || !onPinningBoundary) && 'invisible',\n )}\n aria-label={\n column.getIsPinned() ? `Unfreeze '${header}' column` : `Freeze '${header}' column`\n }\n title={column.getIsPinned() ? 'Unfreeze column' : 'Freeze column'}\n data-bs-toggle=\"tooltip\"\n onClick={() => onTogglePin(column.id)}\n >\n <i className={`bi ${column.getIsPinned() ? 'bi-x' : 'bi-snow'}`} aria-hidden=\"true\" />\n </button>\n </div>\n );\n}\n\nfunction ColumnGroupItem<RowDataModel>({\n column,\n table,\n onTogglePin,\n getIsOnPinningBoundary,\n}: {\n column: Column<RowDataModel>;\n table: Table<RowDataModel>;\n onTogglePin: (columnId: string) => void;\n getIsOnPinningBoundary: (columnId: string) => boolean;\n}) {\n const [isExpanded, setIsExpanded] = useState(false);\n const checkboxRef = useRef<HTMLInputElement>(null);\n\n const leafColumns = column.getLeafColumns();\n const visibleLeafColumns = leafColumns.filter((c) => c.getIsVisible());\n const isAllVisible = visibleLeafColumns.length === leafColumns.length;\n const isSomeVisible = visibleLeafColumns.length > 0 && !isAllVisible;\n\n // Set indeterminate state via ref since it's a DOM property, not an HTML attribute\n useEffect(() => {\n if (checkboxRef.current) {\n checkboxRef.current.indeterminate = isSomeVisible;\n }\n }, [isSomeVisible]);\n\n const handleToggleVisibility = (e: React.ChangeEvent<HTMLInputElement>) => {\n e.preventDefault();\n e.stopPropagation();\n const targetVisibility = !isAllVisible;\n // Batch all visibility changes into a single update\n // Doing rapid state updates caused the state updates to not be applied correctly.\n // See https://github.com/PrairieLearn/PrairieLearn/pull/13989\n table.setColumnVisibility((old) => {\n const newVisibility = { ...old };\n leafColumns.forEach((col) => {\n if (col.getCanHide()) {\n newVisibility[col.id] = targetVisibility;\n }\n });\n return newVisibility;\n });\n };\n\n // Use meta.label if available, otherwise fall back to header or column.id\n const header =\n column.columnDef.meta?.label ??\n (typeof column.columnDef.header === 'string' ? column.columnDef.header : column.id);\n\n return (\n <div className=\"d-flex flex-column\">\n <div className=\"px-2 py-1 d-flex align-items-center justify-content-between\">\n <div className=\"d-flex align-items-center flex-grow-1\">\n <input\n ref={checkboxRef}\n type=\"checkbox\"\n className=\"form-check-input flex-shrink-0\"\n checked={isAllVisible}\n aria-label={`Toggle visibility for group '${header}'`}\n onChange={handleToggleVisibility}\n />\n <button\n type=\"button\"\n className=\"btn btn-link text-decoration-none text-reset w-100 text-start d-flex align-items-center justify-content-between ps-2 py-0 pe-0\"\n aria-expanded={isExpanded}\n onClick={(e) => {\n e.stopPropagation();\n setIsExpanded(!isExpanded);\n }}\n >\n <span className=\"fw-bold text-truncate\">{header}</span>\n <i\n className={clsx(\n 'bi ms-2 text-muted',\n isExpanded ? 'bi-chevron-down' : 'bi-chevron-right',\n )}\n aria-hidden=\"true\"\n />\n </button>\n </div>\n </div>\n {isExpanded && (\n <div className=\"ps-3 border-start ms-3 mb-1\">\n {column.columns.map((childCol) => (\n <ColumnItem\n key={childCol.id}\n column={childCol}\n table={table}\n getIsOnPinningBoundary={getIsOnPinningBoundary}\n onTogglePin={onTogglePin}\n />\n ))}\n </div>\n )}\n </div>\n );\n}\n\nfunction ColumnItem<RowDataModel>({\n column,\n table,\n onTogglePin,\n getIsOnPinningBoundary,\n}: {\n column: Column<RowDataModel>;\n table: Table<RowDataModel>;\n onTogglePin: (columnId: string) => void;\n getIsOnPinningBoundary: (columnId: string) => boolean;\n}) {\n if (column.columns.length > 0) {\n return (\n <ColumnGroupItem\n column={column}\n table={table}\n getIsOnPinningBoundary={getIsOnPinningBoundary}\n onTogglePin={onTogglePin}\n />\n );\n }\n return (\n <ColumnLeafItem\n column={column}\n onPinningBoundary={getIsOnPinningBoundary(column.id)}\n onTogglePin={onTogglePin}\n />\n );\n}\n\ninterface ColumnManagerProps<RowDataModel> {\n table: Table<RowDataModel>;\n topContent?: ReactNode;\n}\n\n/**\n * Ponyfill for `Array.prototype.findLastIndex`, which is not available in the\n * `ES2022` TypeScript lib that we're currently using.\n */\nfunction findLastIndex<T>(arr: T[], predicate: (value: T, index: number) => boolean): number {\n for (let i = arr.length - 1; i >= 0; i--) {\n if (predicate(arr[i], i)) {\n return i;\n }\n }\n return -1;\n}\n\nexport function ColumnManager<RowDataModel>({\n table,\n topContent,\n}: ColumnManagerProps<RowDataModel>) {\n const [activeElementId, setActiveElementId] = useState<string | null>(null);\n const [dropdownOpen, setDropdownOpen] = useState(false);\n const menuRef = useRef<HTMLDivElement>(null);\n const handleTogglePin = (columnId: string) => {\n const currentLeft = table.getState().columnPinning.left ?? [];\n const isPinned = currentLeft.includes(columnId);\n const allLeafColumns = table.getAllLeafColumns();\n const currentColumnIndex = allLeafColumns.findIndex((c) => c.id === columnId);\n let newLeft: string[];\n if (isPinned) {\n // Get the previous column that can be set to unpinned.\n // This is useful since we want to unpin/pin columns that are not shown in the view manager.\n const previousFrozenColumnIndex = findLastIndex(\n allLeafColumns,\n (c, index) => c.getCanHide() && index < currentColumnIndex,\n );\n newLeft = allLeafColumns.slice(0, previousFrozenColumnIndex + 1).map((c) => c.id);\n } else {\n // Pin all columns to the left of the current column.\n const leftColumns = allLeafColumns.slice(0, currentColumnIndex + 1);\n newLeft = leftColumns.map((c) => c.id);\n }\n table.setColumnPinning({ left: newLeft, right: [] });\n setActiveElementId(`${columnId}-pin`);\n };\n\n const isVisibilityChanged = Object.entries(table.getState().columnVisibility).some(\n ([key, value]) => {\n return value !== table.initialState.columnVisibility[key];\n },\n );\n\n const initialPinning = table.initialState.columnPinning.left ?? [];\n const currentPinning = table.getState().columnPinning.left ?? [];\n const isPinningChanged =\n initialPinning.length !== currentPinning.length ||\n initialPinning.some((id) => !currentPinning.includes(id));\n const showResetButton = isVisibilityChanged || isPinningChanged;\n\n const allLeafColumns = table.getAllLeafColumns();\n const pinnedMenuColumns = allLeafColumns.filter(\n (c) => c.getCanHide() && c.getIsPinned() === 'left',\n );\n // Only the first unpinned menu column can be pinned, so we only need to find the first one\n const firstUnpinnedMenuColumn = allLeafColumns.find(\n (c) => c.getCanHide() && c.getIsPinned() !== 'left',\n );\n\n // Determine if a column is on the pinning boundary (can toggle its pin state).\n // - Columns in a group cannot be pinned\n // - Columns after a group cannot be pinned\n // - Only the last pinned menu column can be unpinned\n // - Only the first unpinned menu column can be pinned\n const getIsOnPinningBoundary = (columnId: string) => {\n const column = allLeafColumns.find((c) => c.id === columnId);\n if (!column) return false;\n\n // Columns in a group cannot be pinned\n if (column.parent) return false;\n\n // Check if any column at or before this one in the full column order is in a group\n const columnIdx = allLeafColumns.findIndex((c) => c.id === columnId);\n const hasGroupAtOrBefore = allLeafColumns.slice(0, columnIdx + 1).some((c) => c.parent);\n\n if (column.getIsPinned() === 'left') {\n // Only the last pinned menu column can be unpinned\n return columnId === pinnedMenuColumns[pinnedMenuColumns.length - 1]?.id;\n } else {\n // Cannot pin if there's a group at or before this column\n if (hasGroupAtOrBefore) return false;\n // Only the first unpinned menu column can be pinned\n return columnId === firstUnpinnedMenuColumn?.id;\n }\n };\n\n // Get root columns (for showing hierarchy), but filter to only show unpinned ones\n // We'll show pinned columns separately in the \"Frozen columns\" section\n const unpinnedRootColumns = table.getAllColumns().filter((c) => {\n if (c.depth !== 0) return false;\n // A root column is considered unpinned if all its leaf columns are unpinned\n const leafCols = c.getLeafColumns();\n return (\n leafCols.length > 0 &&\n leafCols.every((leaf) => leaf.getIsPinned() !== 'left' && c.getCanHide())\n );\n });\n\n useEffect(() => {\n // When we use the pin or reset button, we want to refocus to another element.\n // We want this in a useEffect so that this code runs after the component re-renders.\n\n // eslint-disable-next-line react-you-might-not-need-an-effect/no-event-handler\n if (activeElementId) {\n document.getElementById(activeElementId)?.focus();\n }\n }, [activeElementId]);\n\n return (\n <Dropdown\n ref={menuRef}\n autoClose=\"outside\"\n show={dropdownOpen}\n onToggle={(isOpen, _meta) => setDropdownOpen(isOpen)}\n onBlur={(e: React.FocusEvent) => {\n // Since we aren't using role=\"menu\", we need to manually close the dropdown when focus leaves.\n // `relatedTarget` is the element gaining focus.\n if (menuRef.current && !menuRef.current.contains(e.relatedTarget)) {\n setDropdownOpen(false);\n }\n }}\n >\n <Dropdown.Toggle\n // We assume that this component will only appear once per page. If that changes,\n // we'll need to do something to ensure ID uniqueness here.\n id=\"column-manager\"\n variant=\"tanstack-table\"\n >\n <i className=\"bi bi-view-list me-2\" aria-hidden=\"true\" /> View{' '}\n </Dropdown.Toggle>\n <Dropdown.Menu style={{ maxHeight: '60vh', overflowY: 'auto' }}>\n {topContent && (\n <>\n {topContent}\n <Dropdown.Divider />\n </>\n )}\n {pinnedMenuColumns.length > 0 && (\n <>\n <div className=\"px-2 py-1 text-muted small\" role=\"presentation\">\n Frozen columns\n </div>\n <div role=\"group\">\n {/* Only leaf columns can be pinned in the current implementation. */}\n {pinnedMenuColumns.map((column, index) => {\n return (\n <ColumnLeafItem\n key={column.id}\n column={column}\n onPinningBoundary={index === pinnedMenuColumns.length - 1}\n onTogglePin={handleTogglePin}\n />\n );\n })}\n </div>\n <Dropdown.Divider />\n </>\n )}\n {unpinnedRootColumns.length > 0 && (\n <>\n <div role=\"group\">\n {unpinnedRootColumns.map((column) => {\n return (\n <ColumnItem\n key={column.id}\n column={column}\n table={table}\n getIsOnPinningBoundary={getIsOnPinningBoundary}\n onTogglePin={handleTogglePin}\n />\n );\n })}\n </div>\n {showResetButton && <Dropdown.Divider />}\n </>\n )}\n {showResetButton && (\n <div className=\"px-2 py-1\">\n <Button\n variant=\"secondary\"\n size=\"sm\"\n className=\"w-100\"\n aria-label=\"Reset all columns to default visibility and pinning\"\n onClick={() => {\n table.resetColumnVisibility();\n table.resetColumnPinning();\n // Move focus to the column manager button after resetting.\n setActiveElementId('column-manager');\n }}\n >\n <i className=\"bi bi-arrow-counterclockwise me-2\" aria-hidden=\"true\" />\n Reset view\n </Button>\n </div>\n )}\n </Dropdown.Menu>\n </Dropdown>\n );\n}\n"]}
|
|
1
|
+
{"version":3,"file":"ColumnManager.js","sourceRoot":"","sources":["../../src/components/ColumnManager.tsx"],"names":[],"mappings":"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","sourcesContent":["import { type Column, type Table } from '@tanstack/react-table';\nimport clsx from 'clsx';\nimport { type ReactNode, useEffect, useRef, useState } from 'react';\nimport Button from 'react-bootstrap/Button';\nimport Dropdown from 'react-bootstrap/Dropdown';\n\ninterface ColumnMenuItemProps<RowDataModel> {\n column: Column<RowDataModel>;\n onPinningBoundary: boolean;\n onTogglePin: (columnId: string) => void;\n className?: string;\n}\n\nfunction ColumnLeafItem<RowDataModel>({\n column,\n onPinningBoundary = false,\n onTogglePin,\n className,\n}: ColumnMenuItemProps<RowDataModel>) {\n if (!column.getCanHide()) return null;\n\n // Use meta.label if available, otherwise fall back to header or column.id\n const header =\n column.columnDef.meta?.label ??\n (typeof column.columnDef.header === 'string' ? column.columnDef.header : column.id);\n\n return (\n <div\n key={column.id}\n className={clsx('px-2 py-1 d-flex align-items-center justify-content-between', className)}\n >\n <label className=\"form-check me-auto text-nowrap d-flex align-items-stretch\">\n <input\n type=\"checkbox\"\n className=\"form-check-input\"\n checked={column.getIsVisible()}\n disabled={!column.getCanHide()}\n aria-label={column.getIsVisible() ? `Hide '${header}' column` : `Show '${header}' column`}\n aria-describedby={`${column.id}-label`}\n onChange={column.getToggleVisibilityHandler()}\n />\n <span className=\"form-check-label ms-2\" id={`${column.id}-label`}>\n {header}\n </span>\n </label>\n <button\n type=\"button\"\n // Since the HTML changes, but we want to refocus the pin button, we track\n // the active pin button and refocuses it when the column manager is rerendered.\n id={`${column.id}-pin`}\n className={clsx(\n 'btn btn-sm btn-ghost ms-2',\n (!column.getCanPin() || !onPinningBoundary) && 'invisible',\n )}\n aria-label={\n column.getIsPinned() ? `Unfreeze '${header}' column` : `Freeze '${header}' column`\n }\n title={column.getIsPinned() ? 'Unfreeze column' : 'Freeze column'}\n data-bs-toggle=\"tooltip\"\n onClick={() => onTogglePin(column.id)}\n >\n <i className={`bi ${column.getIsPinned() ? 'bi-x' : 'bi-snow'}`} aria-hidden=\"true\" />\n </button>\n </div>\n );\n}\n\nfunction ColumnGroupItem<RowDataModel>({\n column,\n table,\n onTogglePin,\n getIsOnPinningBoundary,\n}: {\n column: Column<RowDataModel>;\n table: Table<RowDataModel>;\n onTogglePin: (columnId: string) => void;\n getIsOnPinningBoundary: (columnId: string) => boolean;\n}) {\n const [isExpanded, setIsExpanded] = useState(false);\n const checkboxRef = useRef<HTMLInputElement>(null);\n\n const leafColumns = column.getLeafColumns();\n const visibleLeafColumns = leafColumns.filter((c) => c.getIsVisible());\n const isAllVisible = visibleLeafColumns.length === leafColumns.length;\n const isSomeVisible = visibleLeafColumns.length > 0 && !isAllVisible;\n\n // Set indeterminate state via ref since it's a DOM property, not an HTML attribute\n useEffect(() => {\n if (checkboxRef.current) {\n checkboxRef.current.indeterminate = isSomeVisible;\n }\n }, [isSomeVisible]);\n\n const handleToggleVisibility = (e: React.ChangeEvent<HTMLInputElement>) => {\n e.preventDefault();\n e.stopPropagation();\n const targetVisibility = !isAllVisible;\n // Batch all visibility changes into a single update\n // Doing rapid state updates caused the state updates to not be applied correctly.\n // See https://github.com/PrairieLearn/PrairieLearn/pull/13989\n table.setColumnVisibility((old) => {\n const newVisibility = { ...old };\n leafColumns.forEach((col) => {\n if (col.getCanHide()) {\n newVisibility[col.id] = targetVisibility;\n }\n });\n return newVisibility;\n });\n };\n\n // Use meta.label if available, otherwise fall back to header or column.id\n const header =\n column.columnDef.meta?.label ??\n (typeof column.columnDef.header === 'string' ? column.columnDef.header : column.id);\n\n return (\n <div className=\"d-flex flex-column\">\n <div className=\"px-2 py-1 d-flex align-items-center justify-content-between\">\n <div className=\"d-flex align-items-center flex-grow-1\">\n <input\n ref={checkboxRef}\n type=\"checkbox\"\n className=\"form-check-input flex-shrink-0\"\n checked={isAllVisible}\n aria-label={`Toggle visibility for group '${header}'`}\n onChange={handleToggleVisibility}\n />\n <button\n type=\"button\"\n className=\"btn btn-link text-decoration-none text-reset w-100 text-start d-flex align-items-center justify-content-between ps-2 py-0 pe-0\"\n aria-expanded={isExpanded}\n onClick={(e) => {\n e.stopPropagation();\n setIsExpanded(!isExpanded);\n }}\n >\n <span className=\"fw-bold text-truncate\">{header}</span>\n <i\n className={clsx(\n 'bi ms-2 text-muted',\n isExpanded ? 'bi-chevron-down' : 'bi-chevron-right',\n )}\n aria-hidden=\"true\"\n />\n </button>\n </div>\n </div>\n {isExpanded && (\n <div className=\"ps-3 border-start ms-3 mb-1\">\n {column.columns.map((childCol) => (\n <ColumnItem\n key={childCol.id}\n column={childCol}\n table={table}\n getIsOnPinningBoundary={getIsOnPinningBoundary}\n onTogglePin={onTogglePin}\n />\n ))}\n </div>\n )}\n </div>\n );\n}\n\nfunction ColumnItem<RowDataModel>({\n column,\n table,\n onTogglePin,\n getIsOnPinningBoundary,\n}: {\n column: Column<RowDataModel>;\n table: Table<RowDataModel>;\n onTogglePin: (columnId: string) => void;\n getIsOnPinningBoundary: (columnId: string) => boolean;\n}) {\n if (column.columns.length > 0) {\n return (\n <ColumnGroupItem\n column={column}\n table={table}\n getIsOnPinningBoundary={getIsOnPinningBoundary}\n onTogglePin={onTogglePin}\n />\n );\n }\n return (\n <ColumnLeafItem\n column={column}\n onPinningBoundary={getIsOnPinningBoundary(column.id)}\n onTogglePin={onTogglePin}\n />\n );\n}\n\ninterface ColumnManagerProps<RowDataModel> {\n table: Table<RowDataModel>;\n topContent?: ReactNode;\n}\n\n/**\n * Ponyfill for `Array.prototype.findLastIndex`, which is not available in the\n * `ES2022` TypeScript lib that we're currently using.\n */\nfunction findLastIndex<T>(arr: T[], predicate: (value: T, index: number) => boolean): number {\n for (let i = arr.length - 1; i >= 0; i--) {\n if (predicate(arr[i], i)) {\n return i;\n }\n }\n return -1;\n}\n\nexport function ColumnManager<RowDataModel>({\n table,\n topContent,\n}: ColumnManagerProps<RowDataModel>) {\n const [activeElementId, setActiveElementId] = useState<string | null>(null);\n const [dropdownOpen, setDropdownOpen] = useState(false);\n const menuRef = useRef<HTMLDivElement>(null);\n const handleTogglePin = (columnId: string) => {\n const currentLeft = table.getState().columnPinning.left ?? [];\n const isPinned = currentLeft.includes(columnId);\n const allLeafColumns = table.getAllLeafColumns();\n const currentColumnIndex = allLeafColumns.findIndex((c) => c.id === columnId);\n let newLeft: string[];\n if (isPinned) {\n // Get the previous column that can be set to unpinned.\n // This is useful since we want to unpin/pin columns that are not shown in the view manager.\n const previousFrozenColumnIndex = findLastIndex(\n allLeafColumns,\n (c, index) => c.getCanHide() && index < currentColumnIndex,\n );\n newLeft = allLeafColumns.slice(0, previousFrozenColumnIndex + 1).map((c) => c.id);\n } else {\n // Pin all columns to the left of the current column.\n const leftColumns = allLeafColumns.slice(0, currentColumnIndex + 1);\n newLeft = leftColumns.map((c) => c.id);\n }\n table.setColumnPinning({ left: newLeft, right: [] });\n setActiveElementId(`${columnId}-pin`);\n };\n\n const isVisibilityChanged = Object.entries(table.getState().columnVisibility).some(\n ([key, value]) => {\n return value !== table.initialState.columnVisibility[key];\n },\n );\n\n const initialPinning = table.initialState.columnPinning.left ?? [];\n const currentPinning = table.getState().columnPinning.left ?? [];\n const isPinningChanged =\n initialPinning.length !== currentPinning.length ||\n initialPinning.some((id) => !currentPinning.includes(id));\n const showResetButton = isVisibilityChanged || isPinningChanged;\n\n const allLeafColumns = table.getAllLeafColumns();\n const pinnedMenuColumns = allLeafColumns.filter(\n (c) => c.getCanHide() && c.getIsPinned() === 'left',\n );\n // Only the first unpinned menu column can be pinned, so we only need to find the first one\n const firstUnpinnedMenuColumn = allLeafColumns.find(\n (c) => c.getCanHide() && c.getIsPinned() !== 'left',\n );\n\n // Determine if a column is on the pinning boundary (can toggle its pin state).\n // - Columns in a group cannot be pinned\n // - Columns after a group cannot be pinned\n // - Only the last pinned menu column can be unpinned\n // - Only the first unpinned menu column can be pinned\n const getIsOnPinningBoundary = (columnId: string) => {\n const column = allLeafColumns.find((c) => c.id === columnId);\n if (!column) return false;\n\n // Columns in a group cannot be pinned\n if (column.parent) return false;\n\n // Check if any column at or before this one in the full column order is in a group\n const columnIdx = allLeafColumns.findIndex((c) => c.id === columnId);\n const hasGroupAtOrBefore = allLeafColumns.slice(0, columnIdx + 1).some((c) => c.parent);\n\n if (column.getIsPinned() === 'left') {\n // Only the last pinned menu column can be unpinned\n return columnId === pinnedMenuColumns[pinnedMenuColumns.length - 1]?.id;\n } else {\n // Cannot pin if there's a group at or before this column\n if (hasGroupAtOrBefore) return false;\n // Only the first unpinned menu column can be pinned\n return columnId === firstUnpinnedMenuColumn?.id;\n }\n };\n\n // Get root columns (for showing hierarchy), but filter to only show unpinned ones\n // We'll show pinned columns separately in the \"Frozen columns\" section\n const unpinnedRootColumns = table.getAllColumns().filter((c) => {\n if (c.depth !== 0) return false;\n // A root column is considered unpinned if all its leaf columns are unpinned\n const leafCols = c.getLeafColumns();\n return (\n leafCols.length > 0 &&\n leafCols.every((leaf) => leaf.getIsPinned() !== 'left' && c.getCanHide())\n );\n });\n\n useEffect(() => {\n // When we use the pin or reset button, we want to refocus to another element.\n // We want this in a useEffect so that this code runs after the component re-renders.\n\n // eslint-disable-next-line react-you-might-not-need-an-effect/no-event-handler\n if (activeElementId) {\n document.getElementById(activeElementId)?.focus();\n }\n }, [activeElementId]);\n\n return (\n <Dropdown\n ref={menuRef}\n autoClose=\"outside\"\n show={dropdownOpen}\n onToggle={(isOpen, _meta) => setDropdownOpen(isOpen)}\n onBlur={(e: React.FocusEvent) => {\n // Since we aren't using role=\"menu\", we need to manually close the dropdown when focus leaves.\n // `relatedTarget` is the element gaining focus.\n if (menuRef.current && !menuRef.current.contains(e.relatedTarget)) {\n setDropdownOpen(false);\n }\n }}\n >\n <Dropdown.Toggle\n // We assume that this component will only appear once per page. If that changes,\n // we'll need to do something to ensure ID uniqueness here.\n id=\"column-manager\"\n variant=\"tanstack-table\"\n >\n <i className=\"bi bi-view-list me-2\" aria-hidden=\"true\" /> View{' '}\n </Dropdown.Toggle>\n <Dropdown.Menu style={{ maxHeight: '60vh', overflowY: 'auto' }}>\n {topContent && (\n <>\n {topContent}\n <Dropdown.Divider />\n </>\n )}\n {pinnedMenuColumns.length > 0 && (\n <>\n <div className=\"px-2 py-1 text-muted small\" role=\"presentation\">\n Frozen columns\n </div>\n <div role=\"group\">\n {/* Only leaf columns can be pinned in the current implementation. */}\n {pinnedMenuColumns.map((column, index) => {\n return (\n <ColumnLeafItem\n key={column.id}\n column={column}\n onPinningBoundary={index === pinnedMenuColumns.length - 1}\n onTogglePin={handleTogglePin}\n />\n );\n })}\n </div>\n <Dropdown.Divider />\n </>\n )}\n {unpinnedRootColumns.length > 0 && (\n <>\n <div role=\"group\">\n {unpinnedRootColumns.map((column) => {\n return (\n <ColumnItem\n key={column.id}\n column={column}\n table={table}\n getIsOnPinningBoundary={getIsOnPinningBoundary}\n onTogglePin={handleTogglePin}\n />\n );\n })}\n </div>\n {showResetButton && <Dropdown.Divider />}\n </>\n )}\n {showResetButton && (\n <div className=\"px-2 py-1\">\n <Button\n variant=\"secondary\"\n size=\"sm\"\n className=\"w-100\"\n aria-label=\"Reset all columns to default visibility and pinning\"\n onClick={() => {\n table.resetColumnVisibility();\n table.resetColumnPinning();\n // Move focus to the column manager button after resetting.\n setActiveElementId('column-manager');\n }}\n >\n <i className=\"bi bi-arrow-counterclockwise me-2\" aria-hidden=\"true\" />\n Reset view\n </Button>\n </div>\n )}\n </Dropdown.Menu>\n </Dropdown>\n );\n}\n"]}
|
|
@@ -9,7 +9,7 @@ export interface ComboBoxItem<T = void> {
|
|
|
9
9
|
/** Text used for filtering (defaults to label). */
|
|
10
10
|
searchableText?: string;
|
|
11
11
|
}
|
|
12
|
-
type ManagedAriaProps = 'children' | 'items' | 'selectedKey' | 'defaultSelectedKey' | 'onSelectionChange' | 'inputValue' | 'defaultInputValue' | 'onInputChange' | 'onOpenChange' | 'menuTrigger' | 'allowsEmptyCollection' | 'isDisabled' | 'isInvalid';
|
|
12
|
+
type ManagedAriaProps = 'children' | 'items' | 'selectedKey' | 'defaultSelectedKey' | 'onSelectionChange' | 'inputValue' | 'defaultInputValue' | 'onInputChange' | 'onOpenChange' | 'menuTrigger' | 'allowsEmptyCollection' | 'isDisabled' | 'isInvalid' | 'value' | 'defaultValue' | 'onChange';
|
|
13
13
|
export interface ComboBoxProps<T = void> extends Omit<AriaComboBoxProps<ComboBoxItem<T>>, ManagedAriaProps> {
|
|
14
14
|
items: ComboBoxItem<T>[];
|
|
15
15
|
value: string | null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ComboBox.d.ts","sourceRoot":"","sources":["../../src/components/ComboBox.tsx"],"names":[],"mappings":"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","sourcesContent":["import clsx from 'clsx';\nimport { type Key, type ReactNode, useMemo, useState } from 'react';\nimport { useFilter } from 'react-aria';\nimport {\n ComboBox as AriaComboBox,\n type ComboBoxProps as AriaComboBoxProps,\n Button,\n FieldError,\n Group,\n Input,\n Label,\n ListBox,\n ListBoxItem,\n Popover,\n Tag,\n TagGroup,\n TagList,\n Text,\n} from 'react-aria-components';\n\n/** An item in the ComboBox or TagPicker dropdown. */\nexport interface ComboBoxItem<T = void> {\n id: string;\n label: string;\n /** Custom data passed to renderItem. */\n data?: T;\n /** Text used for filtering (defaults to label). */\n searchableText?: string;\n}\n\ntype ManagedAriaProps =\n | 'children'\n | 'items'\n | 'selectedKey'\n | 'defaultSelectedKey'\n | 'onSelectionChange'\n | 'inputValue'\n | 'defaultInputValue'\n | 'onInputChange'\n | 'onOpenChange'\n | 'menuTrigger'\n | 'allowsEmptyCollection'\n | 'isDisabled'\n | 'isInvalid';\n\nexport interface ComboBoxProps<T = void> extends Omit<\n AriaComboBoxProps<ComboBoxItem<T>>,\n ManagedAriaProps\n> {\n items: ComboBoxItem<T>[];\n value: string | null;\n onChange: (value: string | null) => void;\n placeholder?: string;\n disabled?: boolean;\n /** Name for hidden form input. */\n name?: string;\n /** ID for the input element. */\n id?: string;\n label?: string;\n description?: string;\n errorMessage?: string;\n renderItem?: (item: ComboBoxItem<T>) => ReactNode;\n}\n\nexport interface TagPickerProps<T = void> extends Omit<\n AriaComboBoxProps<ComboBoxItem<T>>,\n ManagedAriaProps\n> {\n items: ComboBoxItem<T>[];\n value: string[];\n onChange: (value: string[]) => void;\n placeholder?: string;\n disabled?: boolean;\n /** Name for hidden form inputs. */\n name?: string;\n /** ID for the input element. */\n id?: string;\n label?: string;\n description?: string;\n errorMessage?: string;\n renderItem?: (item: ComboBoxItem<T>) => ReactNode;\n renderTag?: (item: ComboBoxItem<T>) => ReactNode;\n}\n\nfunction defaultRenderItem<T>(item: ComboBoxItem<T>) {\n return <span>{item.label}</span>;\n}\n\nfunction defaultRenderTag<T>(item: ComboBoxItem<T>) {\n return <span className=\"badge bg-secondary\">{item.label}</span>;\n}\n\n/**\n * Single-selection combobox with filtering.\n */\nexport function ComboBox<T = void>({\n items,\n value,\n onChange,\n placeholder = 'Select...',\n disabled = false,\n name,\n id,\n label,\n description,\n errorMessage,\n renderItem = defaultRenderItem,\n ...props\n}: ComboBoxProps<T>) {\n const { contains } = useFilter({ sensitivity: 'base' });\n const [isOpen, setIsOpen] = useState(false);\n const [filterText, setFilterText] = useState('');\n\n const selectedItem = useMemo(\n () => items.find((item) => item.id === value) ?? null,\n [items, value],\n );\n\n // Input value is derived: show filter text when open, selected label when closed\n const inputValue = isOpen ? filterText : (selectedItem?.label ?? '');\n\n const filteredItems = useMemo(() => {\n if (!inputValue.trim()) return items;\n return items.filter((item) => {\n const searchable = item.searchableText ?? item.label;\n return contains(searchable, inputValue);\n });\n }, [items, inputValue, contains]);\n\n const handleSelectionChange = (key: Key | null) => {\n const stringKey = typeof key === 'string' ? key : null;\n const newSelectedItem = stringKey ? items.find((item) => item.id === stringKey) : null;\n onChange(stringKey);\n // Set filter text to new label for immediate display before props update\n setFilterText(newSelectedItem?.label ?? '');\n };\n\n const handleInputChange = (inputVal: string) => {\n setFilterText(inputVal);\n if (inputVal === '' && value !== null) {\n onChange(null);\n }\n };\n\n const handleOpenChange = (open: boolean, trigger?: 'focus' | 'input' | 'manual') => {\n setIsOpen(open);\n // Initialize filter text to selected label when opening via focus\n if (open && trigger === 'focus') {\n setFilterText(selectedItem?.label ?? '');\n }\n };\n\n return (\n <div className=\"position-relative\">\n {name && <input name={name} type=\"hidden\" value={value ?? ''} />}\n\n <AriaComboBox\n {...props}\n selectedKey={value}\n inputValue={inputValue}\n isDisabled={disabled}\n isInvalid={!!errorMessage}\n menuTrigger=\"focus\"\n allowsEmptyCollection\n onSelectionChange={handleSelectionChange}\n onInputChange={handleInputChange}\n onOpenChange={handleOpenChange}\n >\n {label && <Label className=\"form-label\">{label}</Label>}\n\n <Group\n className={clsx(\n 'form-control d-flex align-items-center gap-1',\n disabled && 'bg-body-secondary',\n isOpen && 'border-primary shadow-sm',\n errorMessage && 'is-invalid',\n )}\n style={{ minHeight: '38px', cursor: disabled ? 'not-allowed' : 'text' }}\n >\n <Input\n className=\"border-0 flex-grow-1 bg-transparent\"\n id={id}\n placeholder={placeholder}\n style={{ outline: 'none' }}\n />\n <Button aria-label=\"Show suggestions\" className=\"border-0 bg-transparent p-0 ms-auto\">\n <i\n aria-hidden=\"true\"\n className={clsx('bi', isOpen ? 'bi-chevron-up' : 'bi-chevron-down', 'text-muted')}\n />\n </Button>\n </Group>\n\n {description && (\n <Text className=\"form-text text-muted\" slot=\"description\">\n {description}\n </Text>\n )}\n\n <FieldError className=\"invalid-feedback d-block\">{errorMessage}</FieldError>\n\n <Popover\n className=\"dropdown-menu show py-0 overflow-auto\"\n offset={2}\n style={{ maxHeight: '300px', width: 'var(--trigger-width)' }}\n >\n <ListBox\n className=\"list-unstyled m-0\"\n items={filteredItems}\n renderEmptyState={() => (\n <div className=\"dropdown-item text-muted\">No options found</div>\n )}\n >\n {(item) => (\n <ListBoxItem\n id={item.id}\n className={({ isFocused, isSelected }) =>\n clsx(\n 'dropdown-item d-flex align-items-center gap-2',\n isFocused && 'active',\n isSelected && 'fw-semibold',\n )\n }\n style={{ cursor: 'pointer' }}\n textValue={item.label}\n >\n <span className=\"flex-grow-1\">{renderItem(item)}</span>\n </ListBoxItem>\n )}\n </ListBox>\n </Popover>\n </AriaComboBox>\n </div>\n );\n}\n\n/**\n * Multi-selection combobox with removable tags.\n */\nexport function TagPicker<T = void>({\n items,\n value,\n onChange,\n placeholder = 'Select...',\n disabled = false,\n name,\n id,\n label,\n description,\n errorMessage,\n renderItem = defaultRenderItem,\n renderTag = defaultRenderTag,\n ...props\n}: TagPickerProps<T>) {\n const { contains } = useFilter({ sensitivity: 'base' });\n const [inputValue, setInputValue] = useState('');\n const [isOpen, setIsOpen] = useState(false);\n\n const selectedItems = useMemo(\n () => items.filter((item) => value.includes(item.id)),\n [items, value],\n );\n\n const filteredItems = useMemo(() => {\n if (!inputValue.trim()) return items;\n return items.filter((item) => {\n const searchable = item.searchableText ?? item.label;\n return contains(searchable, inputValue);\n });\n }, [items, inputValue, contains]);\n\n const handleRemoveTag = (keys: Set<Key>) => {\n const newValue = value.filter((v) => !keys.has(v));\n onChange(newValue);\n };\n\n const handleSelect = (key: Key | null) => {\n const itemId = typeof key === 'string' ? key : null;\n if (!itemId) return;\n if (value.includes(itemId)) {\n onChange(value.filter((v) => v !== itemId));\n } else {\n onChange([...value, itemId]);\n }\n setInputValue('');\n };\n\n return (\n <div className=\"position-relative\">\n {name &&\n (selectedItems.length > 0 ? (\n selectedItems.map((item) => (\n <input key={item.id} name={name} type=\"hidden\" value={item.id} />\n ))\n ) : (\n <input name={name} type=\"hidden\" value=\"\" />\n ))}\n\n <AriaComboBox\n {...props}\n inputValue={inputValue}\n isDisabled={disabled}\n isInvalid={!!errorMessage}\n menuTrigger=\"focus\"\n selectedKey={null}\n allowsEmptyCollection\n onInputChange={setInputValue}\n onOpenChange={setIsOpen}\n onSelectionChange={handleSelect}\n >\n {label && <Label className=\"form-label\">{label}</Label>}\n\n <Group\n className={clsx(\n 'form-control d-flex flex-wrap align-items-center gap-1',\n disabled && 'bg-body-secondary',\n isOpen && 'border-primary shadow-sm',\n errorMessage && 'is-invalid',\n )}\n style={{ minHeight: '38px', cursor: disabled ? 'not-allowed' : 'text' }}\n >\n {selectedItems.length > 0 && (\n <TagGroup\n aria-label=\"Selected items\"\n onRemove={!disabled ? handleRemoveTag : undefined}\n >\n <TagList>\n {selectedItems.map((item) => (\n <Tag\n key={item.id}\n id={item.id}\n className=\"d-inline-flex align-items-center\"\n style={{ lineHeight: 1.2 }}\n textValue={item.label}\n >\n {renderTag(item)}\n {!disabled && (\n <Button\n aria-label={`Remove ${item.label}`}\n className=\"btn-close btn-close-sm ms-1 p-0 border-0 bg-transparent\"\n slot=\"remove\"\n style={{ fontSize: '0.6rem' }}\n />\n )}\n </Tag>\n ))}\n </TagList>\n </TagGroup>\n )}\n\n <div className=\"flex-grow-1 d-flex align-items-center\">\n <Input\n className=\"border-0 flex-grow-1 bg-transparent\"\n id={id}\n placeholder={selectedItems.length === 0 ? placeholder : ''}\n style={{ outline: 'none', minWidth: '60px' }}\n />\n <Button aria-label=\"Show suggestions\" className=\"border-0 bg-transparent p-0 ms-auto\">\n <i\n aria-hidden=\"true\"\n className={clsx('bi', isOpen ? 'bi-chevron-up' : 'bi-chevron-down', 'text-muted')}\n />\n </Button>\n </div>\n </Group>\n\n {description && (\n <Text className=\"form-text text-muted\" slot=\"description\">\n {description}\n </Text>\n )}\n\n <FieldError className=\"invalid-feedback d-block\">{errorMessage}</FieldError>\n\n <Popover\n className=\"dropdown-menu show py-0 overflow-auto\"\n offset={2}\n style={{ maxHeight: '300px', width: 'var(--trigger-width)' }}\n >\n <ListBox\n className=\"list-unstyled m-0\"\n items={filteredItems}\n renderEmptyState={() => (\n <div className=\"dropdown-item text-muted\">No options found</div>\n )}\n >\n {(item) => {\n const isSelected = value.includes(item.id);\n return (\n <ListBoxItem\n id={item.id}\n className={({ isFocused }) =>\n clsx('dropdown-item d-flex align-items-center gap-2', isFocused && 'active')\n }\n style={{ cursor: 'pointer' }}\n textValue={item.label}\n >\n <input\n checked={isSelected}\n className=\"form-check-input m-0\"\n tabIndex={-1}\n type=\"checkbox\"\n readOnly\n />\n <div className=\"flex-grow-1\">{renderItem(item)}</div>\n </ListBoxItem>\n );\n }}\n </ListBox>\n </Popover>\n </AriaComboBox>\n </div>\n );\n}\n"]}
|
|
1
|
+
{"version":3,"file":"ComboBox.d.ts","sourceRoot":"","sources":["../../src/components/ComboBox.tsx"],"names":[],"mappings":"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","sourcesContent":["import clsx from 'clsx';\nimport { type Key, type ReactNode, useMemo, useState } from 'react';\nimport { useFilter } from 'react-aria';\nimport {\n ComboBox as AriaComboBox,\n type ComboBoxProps as AriaComboBoxProps,\n Button,\n FieldError,\n Group,\n Input,\n Label,\n ListBox,\n ListBoxItem,\n Popover,\n Tag,\n TagGroup,\n TagList,\n Text,\n} from 'react-aria-components';\n\n/** An item in the ComboBox or TagPicker dropdown. */\nexport interface ComboBoxItem<T = void> {\n id: string;\n label: string;\n /** Custom data passed to renderItem. */\n data?: T;\n /** Text used for filtering (defaults to label). */\n searchableText?: string;\n}\n\ntype ManagedAriaProps =\n | 'children'\n | 'items'\n | 'selectedKey'\n | 'defaultSelectedKey'\n | 'onSelectionChange'\n | 'inputValue'\n | 'defaultInputValue'\n | 'onInputChange'\n | 'onOpenChange'\n | 'menuTrigger'\n | 'allowsEmptyCollection'\n | 'isDisabled'\n | 'isInvalid'\n | 'value'\n | 'defaultValue'\n | 'onChange';\n\nexport interface ComboBoxProps<T = void> extends Omit<\n AriaComboBoxProps<ComboBoxItem<T>>,\n ManagedAriaProps\n> {\n items: ComboBoxItem<T>[];\n value: string | null;\n onChange: (value: string | null) => void;\n placeholder?: string;\n disabled?: boolean;\n /** Name for hidden form input. */\n name?: string;\n /** ID for the input element. */\n id?: string;\n label?: string;\n description?: string;\n errorMessage?: string;\n renderItem?: (item: ComboBoxItem<T>) => ReactNode;\n}\n\nexport interface TagPickerProps<T = void> extends Omit<\n AriaComboBoxProps<ComboBoxItem<T>>,\n ManagedAriaProps\n> {\n items: ComboBoxItem<T>[];\n value: string[];\n onChange: (value: string[]) => void;\n placeholder?: string;\n disabled?: boolean;\n /** Name for hidden form inputs. */\n name?: string;\n /** ID for the input element. */\n id?: string;\n label?: string;\n description?: string;\n errorMessage?: string;\n renderItem?: (item: ComboBoxItem<T>) => ReactNode;\n renderTag?: (item: ComboBoxItem<T>) => ReactNode;\n}\n\nfunction defaultRenderItem<T>(item: ComboBoxItem<T>) {\n return <span>{item.label}</span>;\n}\n\nfunction defaultRenderTag<T>(item: ComboBoxItem<T>) {\n return <span className=\"badge bg-secondary\">{item.label}</span>;\n}\n\n/**\n * Single-selection combobox with filtering.\n */\nexport function ComboBox<T = void>({\n items,\n value,\n onChange,\n placeholder = 'Select...',\n disabled = false,\n name,\n id,\n label,\n description,\n errorMessage,\n renderItem = defaultRenderItem,\n ...props\n}: ComboBoxProps<T>) {\n const { contains } = useFilter({ sensitivity: 'base' });\n const [isOpen, setIsOpen] = useState(false);\n const [filterText, setFilterText] = useState('');\n\n const selectedItem = useMemo(\n () => items.find((item) => item.id === value) ?? null,\n [items, value],\n );\n\n // Input value is derived: show filter text when open, selected label when closed\n const inputValue = isOpen ? filterText : (selectedItem?.label ?? '');\n\n const filteredItems = useMemo(() => {\n if (!inputValue.trim()) return items;\n return items.filter((item) => {\n const searchable = item.searchableText ?? item.label;\n return contains(searchable, inputValue);\n });\n }, [items, inputValue, contains]);\n\n const handleSelectionChange = (key: Key | null) => {\n const stringKey = typeof key === 'string' ? key : null;\n const newSelectedItem = stringKey ? items.find((item) => item.id === stringKey) : null;\n onChange(stringKey);\n // Set filter text to new label for immediate display before props update\n setFilterText(newSelectedItem?.label ?? '');\n };\n\n const handleInputChange = (inputVal: string) => {\n setFilterText(inputVal);\n if (inputVal === '' && value !== null) {\n onChange(null);\n }\n };\n\n const handleOpenChange = (open: boolean, trigger?: 'focus' | 'input' | 'manual') => {\n setIsOpen(open);\n // Initialize filter text to selected label when opening via focus\n if (open && trigger === 'focus') {\n setFilterText(selectedItem?.label ?? '');\n }\n };\n\n return (\n <div className=\"position-relative\">\n {name && <input name={name} type=\"hidden\" value={value ?? ''} />}\n\n <AriaComboBox\n {...props}\n selectedKey={value}\n inputValue={inputValue}\n isDisabled={disabled}\n isInvalid={!!errorMessage}\n menuTrigger=\"focus\"\n allowsEmptyCollection\n onSelectionChange={handleSelectionChange}\n onInputChange={handleInputChange}\n onOpenChange={handleOpenChange}\n >\n {label && <Label className=\"form-label\">{label}</Label>}\n\n <Group\n className={clsx(\n 'form-control d-flex align-items-center gap-1',\n disabled && 'bg-body-secondary',\n isOpen && 'border-primary shadow-sm',\n errorMessage && 'is-invalid',\n )}\n style={{ minHeight: '38px', cursor: disabled ? 'not-allowed' : 'text' }}\n >\n <Input\n className=\"border-0 flex-grow-1 bg-transparent\"\n id={id}\n placeholder={placeholder}\n style={{ outline: 'none' }}\n />\n <Button aria-label=\"Show suggestions\" className=\"border-0 bg-transparent p-0 ms-auto\">\n <i\n aria-hidden=\"true\"\n className={clsx('bi', isOpen ? 'bi-chevron-up' : 'bi-chevron-down', 'text-muted')}\n />\n </Button>\n </Group>\n\n {description && (\n <Text className=\"form-text text-muted\" slot=\"description\">\n {description}\n </Text>\n )}\n\n <FieldError className=\"invalid-feedback d-block\">{errorMessage}</FieldError>\n\n <Popover\n className=\"dropdown-menu show py-0 overflow-auto\"\n offset={2}\n style={{ maxHeight: '300px', width: 'var(--trigger-width)' }}\n >\n <ListBox\n className=\"list-unstyled m-0\"\n items={filteredItems}\n renderEmptyState={() => (\n <div className=\"dropdown-item text-muted\">No options found</div>\n )}\n >\n {(item) => (\n <ListBoxItem\n id={item.id}\n className={({ isFocused, isSelected }) =>\n clsx(\n 'dropdown-item d-flex align-items-center gap-2',\n isFocused && 'active',\n isSelected && 'fw-semibold',\n )\n }\n style={{ cursor: 'pointer' }}\n textValue={item.label}\n >\n <span className=\"flex-grow-1\">{renderItem(item)}</span>\n </ListBoxItem>\n )}\n </ListBox>\n </Popover>\n </AriaComboBox>\n </div>\n );\n}\n\n/**\n * Multi-selection combobox with removable tags.\n */\nexport function TagPicker<T = void>({\n items,\n value,\n onChange,\n placeholder = 'Select...',\n disabled = false,\n name,\n id,\n label,\n description,\n errorMessage,\n renderItem = defaultRenderItem,\n renderTag = defaultRenderTag,\n ...props\n}: TagPickerProps<T>) {\n const { contains } = useFilter({ sensitivity: 'base' });\n const [inputValue, setInputValue] = useState('');\n const [isOpen, setIsOpen] = useState(false);\n\n const selectedItems = useMemo(\n () => items.filter((item) => value.includes(item.id)),\n [items, value],\n );\n\n const filteredItems = useMemo(() => {\n if (!inputValue.trim()) return items;\n return items.filter((item) => {\n const searchable = item.searchableText ?? item.label;\n return contains(searchable, inputValue);\n });\n }, [items, inputValue, contains]);\n\n const handleRemoveTag = (keys: Set<Key>) => {\n const newValue = value.filter((v) => !keys.has(v));\n onChange(newValue);\n };\n\n const handleSelect = (key: Key | null) => {\n const itemId = typeof key === 'string' ? key : null;\n if (!itemId) return;\n if (value.includes(itemId)) {\n onChange(value.filter((v) => v !== itemId));\n } else {\n onChange([...value, itemId]);\n }\n setInputValue('');\n };\n\n return (\n <div className=\"position-relative\">\n {name &&\n (selectedItems.length > 0 ? (\n selectedItems.map((item) => (\n <input key={item.id} name={name} type=\"hidden\" value={item.id} />\n ))\n ) : (\n <input name={name} type=\"hidden\" value=\"\" />\n ))}\n\n <AriaComboBox\n {...props}\n inputValue={inputValue}\n isDisabled={disabled}\n isInvalid={!!errorMessage}\n menuTrigger=\"focus\"\n selectedKey={null}\n allowsEmptyCollection\n onInputChange={setInputValue}\n onOpenChange={setIsOpen}\n onSelectionChange={handleSelect}\n >\n {label && <Label className=\"form-label\">{label}</Label>}\n\n <Group\n className={clsx(\n 'form-control d-flex flex-wrap align-items-center gap-1',\n disabled && 'bg-body-secondary',\n isOpen && 'border-primary shadow-sm',\n errorMessage && 'is-invalid',\n )}\n style={{ minHeight: '38px', cursor: disabled ? 'not-allowed' : 'text' }}\n >\n {selectedItems.length > 0 && (\n <TagGroup\n aria-label=\"Selected items\"\n onRemove={!disabled ? handleRemoveTag : undefined}\n >\n <TagList>\n {selectedItems.map((item) => (\n <Tag\n key={item.id}\n id={item.id}\n className=\"d-inline-flex align-items-center\"\n style={{ lineHeight: 1.2 }}\n textValue={item.label}\n >\n {renderTag(item)}\n {!disabled && (\n <Button\n aria-label={`Remove ${item.label}`}\n className=\"btn-close btn-close-sm ms-1 p-0 border-0 bg-transparent\"\n slot=\"remove\"\n style={{ fontSize: '0.6rem' }}\n />\n )}\n </Tag>\n ))}\n </TagList>\n </TagGroup>\n )}\n\n <div className=\"flex-grow-1 d-flex align-items-center\">\n <Input\n className=\"border-0 flex-grow-1 bg-transparent\"\n id={id}\n placeholder={selectedItems.length === 0 ? placeholder : ''}\n style={{ outline: 'none', minWidth: '60px' }}\n />\n <Button aria-label=\"Show suggestions\" className=\"border-0 bg-transparent p-0 ms-auto\">\n <i\n aria-hidden=\"true\"\n className={clsx('bi', isOpen ? 'bi-chevron-up' : 'bi-chevron-down', 'text-muted')}\n />\n </Button>\n </div>\n </Group>\n\n {description && (\n <Text className=\"form-text text-muted\" slot=\"description\">\n {description}\n </Text>\n )}\n\n <FieldError className=\"invalid-feedback d-block\">{errorMessage}</FieldError>\n\n <Popover\n className=\"dropdown-menu show py-0 overflow-auto\"\n offset={2}\n style={{ maxHeight: '300px', width: 'var(--trigger-width)' }}\n >\n <ListBox\n className=\"list-unstyled m-0\"\n items={filteredItems}\n renderEmptyState={() => (\n <div className=\"dropdown-item text-muted\">No options found</div>\n )}\n >\n {(item) => {\n const isSelected = value.includes(item.id);\n return (\n <ListBoxItem\n id={item.id}\n className={({ isFocused }) =>\n clsx('dropdown-item d-flex align-items-center gap-2', isFocused && 'active')\n }\n style={{ cursor: 'pointer' }}\n textValue={item.label}\n >\n <input\n checked={isSelected}\n className=\"form-check-input m-0\"\n tabIndex={-1}\n type=\"checkbox\"\n readOnly\n />\n <div className=\"flex-grow-1\">{renderItem(item)}</div>\n </ListBoxItem>\n );\n }}\n </ListBox>\n </Popover>\n </AriaComboBox>\n </div>\n );\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ComboBox.js","sourceRoot":"","sources":["../../src/components/ComboBox.tsx"],"names":[],"mappings":"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","sourcesContent":["import clsx from 'clsx';\nimport { type Key, type ReactNode, useMemo, useState } from 'react';\nimport { useFilter } from 'react-aria';\nimport {\n ComboBox as AriaComboBox,\n type ComboBoxProps as AriaComboBoxProps,\n Button,\n FieldError,\n Group,\n Input,\n Label,\n ListBox,\n ListBoxItem,\n Popover,\n Tag,\n TagGroup,\n TagList,\n Text,\n} from 'react-aria-components';\n\n/** An item in the ComboBox or TagPicker dropdown. */\nexport interface ComboBoxItem<T = void> {\n id: string;\n label: string;\n /** Custom data passed to renderItem. */\n data?: T;\n /** Text used for filtering (defaults to label). */\n searchableText?: string;\n}\n\ntype ManagedAriaProps =\n | 'children'\n | 'items'\n | 'selectedKey'\n | 'defaultSelectedKey'\n | 'onSelectionChange'\n | 'inputValue'\n | 'defaultInputValue'\n | 'onInputChange'\n | 'onOpenChange'\n | 'menuTrigger'\n | 'allowsEmptyCollection'\n | 'isDisabled'\n | 'isInvalid';\n\nexport interface ComboBoxProps<T = void> extends Omit<\n AriaComboBoxProps<ComboBoxItem<T>>,\n ManagedAriaProps\n> {\n items: ComboBoxItem<T>[];\n value: string | null;\n onChange: (value: string | null) => void;\n placeholder?: string;\n disabled?: boolean;\n /** Name for hidden form input. */\n name?: string;\n /** ID for the input element. */\n id?: string;\n label?: string;\n description?: string;\n errorMessage?: string;\n renderItem?: (item: ComboBoxItem<T>) => ReactNode;\n}\n\nexport interface TagPickerProps<T = void> extends Omit<\n AriaComboBoxProps<ComboBoxItem<T>>,\n ManagedAriaProps\n> {\n items: ComboBoxItem<T>[];\n value: string[];\n onChange: (value: string[]) => void;\n placeholder?: string;\n disabled?: boolean;\n /** Name for hidden form inputs. */\n name?: string;\n /** ID for the input element. */\n id?: string;\n label?: string;\n description?: string;\n errorMessage?: string;\n renderItem?: (item: ComboBoxItem<T>) => ReactNode;\n renderTag?: (item: ComboBoxItem<T>) => ReactNode;\n}\n\nfunction defaultRenderItem<T>(item: ComboBoxItem<T>) {\n return <span>{item.label}</span>;\n}\n\nfunction defaultRenderTag<T>(item: ComboBoxItem<T>) {\n return <span className=\"badge bg-secondary\">{item.label}</span>;\n}\n\n/**\n * Single-selection combobox with filtering.\n */\nexport function ComboBox<T = void>({\n items,\n value,\n onChange,\n placeholder = 'Select...',\n disabled = false,\n name,\n id,\n label,\n description,\n errorMessage,\n renderItem = defaultRenderItem,\n ...props\n}: ComboBoxProps<T>) {\n const { contains } = useFilter({ sensitivity: 'base' });\n const [isOpen, setIsOpen] = useState(false);\n const [filterText, setFilterText] = useState('');\n\n const selectedItem = useMemo(\n () => items.find((item) => item.id === value) ?? null,\n [items, value],\n );\n\n // Input value is derived: show filter text when open, selected label when closed\n const inputValue = isOpen ? filterText : (selectedItem?.label ?? '');\n\n const filteredItems = useMemo(() => {\n if (!inputValue.trim()) return items;\n return items.filter((item) => {\n const searchable = item.searchableText ?? item.label;\n return contains(searchable, inputValue);\n });\n }, [items, inputValue, contains]);\n\n const handleSelectionChange = (key: Key | null) => {\n const stringKey = typeof key === 'string' ? key : null;\n const newSelectedItem = stringKey ? items.find((item) => item.id === stringKey) : null;\n onChange(stringKey);\n // Set filter text to new label for immediate display before props update\n setFilterText(newSelectedItem?.label ?? '');\n };\n\n const handleInputChange = (inputVal: string) => {\n setFilterText(inputVal);\n if (inputVal === '' && value !== null) {\n onChange(null);\n }\n };\n\n const handleOpenChange = (open: boolean, trigger?: 'focus' | 'input' | 'manual') => {\n setIsOpen(open);\n // Initialize filter text to selected label when opening via focus\n if (open && trigger === 'focus') {\n setFilterText(selectedItem?.label ?? '');\n }\n };\n\n return (\n <div className=\"position-relative\">\n {name && <input name={name} type=\"hidden\" value={value ?? ''} />}\n\n <AriaComboBox\n {...props}\n selectedKey={value}\n inputValue={inputValue}\n isDisabled={disabled}\n isInvalid={!!errorMessage}\n menuTrigger=\"focus\"\n allowsEmptyCollection\n onSelectionChange={handleSelectionChange}\n onInputChange={handleInputChange}\n onOpenChange={handleOpenChange}\n >\n {label && <Label className=\"form-label\">{label}</Label>}\n\n <Group\n className={clsx(\n 'form-control d-flex align-items-center gap-1',\n disabled && 'bg-body-secondary',\n isOpen && 'border-primary shadow-sm',\n errorMessage && 'is-invalid',\n )}\n style={{ minHeight: '38px', cursor: disabled ? 'not-allowed' : 'text' }}\n >\n <Input\n className=\"border-0 flex-grow-1 bg-transparent\"\n id={id}\n placeholder={placeholder}\n style={{ outline: 'none' }}\n />\n <Button aria-label=\"Show suggestions\" className=\"border-0 bg-transparent p-0 ms-auto\">\n <i\n aria-hidden=\"true\"\n className={clsx('bi', isOpen ? 'bi-chevron-up' : 'bi-chevron-down', 'text-muted')}\n />\n </Button>\n </Group>\n\n {description && (\n <Text className=\"form-text text-muted\" slot=\"description\">\n {description}\n </Text>\n )}\n\n <FieldError className=\"invalid-feedback d-block\">{errorMessage}</FieldError>\n\n <Popover\n className=\"dropdown-menu show py-0 overflow-auto\"\n offset={2}\n style={{ maxHeight: '300px', width: 'var(--trigger-width)' }}\n >\n <ListBox\n className=\"list-unstyled m-0\"\n items={filteredItems}\n renderEmptyState={() => (\n <div className=\"dropdown-item text-muted\">No options found</div>\n )}\n >\n {(item) => (\n <ListBoxItem\n id={item.id}\n className={({ isFocused, isSelected }) =>\n clsx(\n 'dropdown-item d-flex align-items-center gap-2',\n isFocused && 'active',\n isSelected && 'fw-semibold',\n )\n }\n style={{ cursor: 'pointer' }}\n textValue={item.label}\n >\n <span className=\"flex-grow-1\">{renderItem(item)}</span>\n </ListBoxItem>\n )}\n </ListBox>\n </Popover>\n </AriaComboBox>\n </div>\n );\n}\n\n/**\n * Multi-selection combobox with removable tags.\n */\nexport function TagPicker<T = void>({\n items,\n value,\n onChange,\n placeholder = 'Select...',\n disabled = false,\n name,\n id,\n label,\n description,\n errorMessage,\n renderItem = defaultRenderItem,\n renderTag = defaultRenderTag,\n ...props\n}: TagPickerProps<T>) {\n const { contains } = useFilter({ sensitivity: 'base' });\n const [inputValue, setInputValue] = useState('');\n const [isOpen, setIsOpen] = useState(false);\n\n const selectedItems = useMemo(\n () => items.filter((item) => value.includes(item.id)),\n [items, value],\n );\n\n const filteredItems = useMemo(() => {\n if (!inputValue.trim()) return items;\n return items.filter((item) => {\n const searchable = item.searchableText ?? item.label;\n return contains(searchable, inputValue);\n });\n }, [items, inputValue, contains]);\n\n const handleRemoveTag = (keys: Set<Key>) => {\n const newValue = value.filter((v) => !keys.has(v));\n onChange(newValue);\n };\n\n const handleSelect = (key: Key | null) => {\n const itemId = typeof key === 'string' ? key : null;\n if (!itemId) return;\n if (value.includes(itemId)) {\n onChange(value.filter((v) => v !== itemId));\n } else {\n onChange([...value, itemId]);\n }\n setInputValue('');\n };\n\n return (\n <div className=\"position-relative\">\n {name &&\n (selectedItems.length > 0 ? (\n selectedItems.map((item) => (\n <input key={item.id} name={name} type=\"hidden\" value={item.id} />\n ))\n ) : (\n <input name={name} type=\"hidden\" value=\"\" />\n ))}\n\n <AriaComboBox\n {...props}\n inputValue={inputValue}\n isDisabled={disabled}\n isInvalid={!!errorMessage}\n menuTrigger=\"focus\"\n selectedKey={null}\n allowsEmptyCollection\n onInputChange={setInputValue}\n onOpenChange={setIsOpen}\n onSelectionChange={handleSelect}\n >\n {label && <Label className=\"form-label\">{label}</Label>}\n\n <Group\n className={clsx(\n 'form-control d-flex flex-wrap align-items-center gap-1',\n disabled && 'bg-body-secondary',\n isOpen && 'border-primary shadow-sm',\n errorMessage && 'is-invalid',\n )}\n style={{ minHeight: '38px', cursor: disabled ? 'not-allowed' : 'text' }}\n >\n {selectedItems.length > 0 && (\n <TagGroup\n aria-label=\"Selected items\"\n onRemove={!disabled ? handleRemoveTag : undefined}\n >\n <TagList>\n {selectedItems.map((item) => (\n <Tag\n key={item.id}\n id={item.id}\n className=\"d-inline-flex align-items-center\"\n style={{ lineHeight: 1.2 }}\n textValue={item.label}\n >\n {renderTag(item)}\n {!disabled && (\n <Button\n aria-label={`Remove ${item.label}`}\n className=\"btn-close btn-close-sm ms-1 p-0 border-0 bg-transparent\"\n slot=\"remove\"\n style={{ fontSize: '0.6rem' }}\n />\n )}\n </Tag>\n ))}\n </TagList>\n </TagGroup>\n )}\n\n <div className=\"flex-grow-1 d-flex align-items-center\">\n <Input\n className=\"border-0 flex-grow-1 bg-transparent\"\n id={id}\n placeholder={selectedItems.length === 0 ? placeholder : ''}\n style={{ outline: 'none', minWidth: '60px' }}\n />\n <Button aria-label=\"Show suggestions\" className=\"border-0 bg-transparent p-0 ms-auto\">\n <i\n aria-hidden=\"true\"\n className={clsx('bi', isOpen ? 'bi-chevron-up' : 'bi-chevron-down', 'text-muted')}\n />\n </Button>\n </div>\n </Group>\n\n {description && (\n <Text className=\"form-text text-muted\" slot=\"description\">\n {description}\n </Text>\n )}\n\n <FieldError className=\"invalid-feedback d-block\">{errorMessage}</FieldError>\n\n <Popover\n className=\"dropdown-menu show py-0 overflow-auto\"\n offset={2}\n style={{ maxHeight: '300px', width: 'var(--trigger-width)' }}\n >\n <ListBox\n className=\"list-unstyled m-0\"\n items={filteredItems}\n renderEmptyState={() => (\n <div className=\"dropdown-item text-muted\">No options found</div>\n )}\n >\n {(item) => {\n const isSelected = value.includes(item.id);\n return (\n <ListBoxItem\n id={item.id}\n className={({ isFocused }) =>\n clsx('dropdown-item d-flex align-items-center gap-2', isFocused && 'active')\n }\n style={{ cursor: 'pointer' }}\n textValue={item.label}\n >\n <input\n checked={isSelected}\n className=\"form-check-input m-0\"\n tabIndex={-1}\n type=\"checkbox\"\n readOnly\n />\n <div className=\"flex-grow-1\">{renderItem(item)}</div>\n </ListBoxItem>\n );\n }}\n </ListBox>\n </Popover>\n </AriaComboBox>\n </div>\n );\n}\n"]}
|
|
1
|
+
{"version":3,"file":"ComboBox.js","sourceRoot":"","sources":["../../src/components/ComboBox.tsx"],"names":[],"mappings":"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","sourcesContent":["import clsx from 'clsx';\nimport { type Key, type ReactNode, useMemo, useState } from 'react';\nimport { useFilter } from 'react-aria';\nimport {\n ComboBox as AriaComboBox,\n type ComboBoxProps as AriaComboBoxProps,\n Button,\n FieldError,\n Group,\n Input,\n Label,\n ListBox,\n ListBoxItem,\n Popover,\n Tag,\n TagGroup,\n TagList,\n Text,\n} from 'react-aria-components';\n\n/** An item in the ComboBox or TagPicker dropdown. */\nexport interface ComboBoxItem<T = void> {\n id: string;\n label: string;\n /** Custom data passed to renderItem. */\n data?: T;\n /** Text used for filtering (defaults to label). */\n searchableText?: string;\n}\n\ntype ManagedAriaProps =\n | 'children'\n | 'items'\n | 'selectedKey'\n | 'defaultSelectedKey'\n | 'onSelectionChange'\n | 'inputValue'\n | 'defaultInputValue'\n | 'onInputChange'\n | 'onOpenChange'\n | 'menuTrigger'\n | 'allowsEmptyCollection'\n | 'isDisabled'\n | 'isInvalid'\n | 'value'\n | 'defaultValue'\n | 'onChange';\n\nexport interface ComboBoxProps<T = void> extends Omit<\n AriaComboBoxProps<ComboBoxItem<T>>,\n ManagedAriaProps\n> {\n items: ComboBoxItem<T>[];\n value: string | null;\n onChange: (value: string | null) => void;\n placeholder?: string;\n disabled?: boolean;\n /** Name for hidden form input. */\n name?: string;\n /** ID for the input element. */\n id?: string;\n label?: string;\n description?: string;\n errorMessage?: string;\n renderItem?: (item: ComboBoxItem<T>) => ReactNode;\n}\n\nexport interface TagPickerProps<T = void> extends Omit<\n AriaComboBoxProps<ComboBoxItem<T>>,\n ManagedAriaProps\n> {\n items: ComboBoxItem<T>[];\n value: string[];\n onChange: (value: string[]) => void;\n placeholder?: string;\n disabled?: boolean;\n /** Name for hidden form inputs. */\n name?: string;\n /** ID for the input element. */\n id?: string;\n label?: string;\n description?: string;\n errorMessage?: string;\n renderItem?: (item: ComboBoxItem<T>) => ReactNode;\n renderTag?: (item: ComboBoxItem<T>) => ReactNode;\n}\n\nfunction defaultRenderItem<T>(item: ComboBoxItem<T>) {\n return <span>{item.label}</span>;\n}\n\nfunction defaultRenderTag<T>(item: ComboBoxItem<T>) {\n return <span className=\"badge bg-secondary\">{item.label}</span>;\n}\n\n/**\n * Single-selection combobox with filtering.\n */\nexport function ComboBox<T = void>({\n items,\n value,\n onChange,\n placeholder = 'Select...',\n disabled = false,\n name,\n id,\n label,\n description,\n errorMessage,\n renderItem = defaultRenderItem,\n ...props\n}: ComboBoxProps<T>) {\n const { contains } = useFilter({ sensitivity: 'base' });\n const [isOpen, setIsOpen] = useState(false);\n const [filterText, setFilterText] = useState('');\n\n const selectedItem = useMemo(\n () => items.find((item) => item.id === value) ?? null,\n [items, value],\n );\n\n // Input value is derived: show filter text when open, selected label when closed\n const inputValue = isOpen ? filterText : (selectedItem?.label ?? '');\n\n const filteredItems = useMemo(() => {\n if (!inputValue.trim()) return items;\n return items.filter((item) => {\n const searchable = item.searchableText ?? item.label;\n return contains(searchable, inputValue);\n });\n }, [items, inputValue, contains]);\n\n const handleSelectionChange = (key: Key | null) => {\n const stringKey = typeof key === 'string' ? key : null;\n const newSelectedItem = stringKey ? items.find((item) => item.id === stringKey) : null;\n onChange(stringKey);\n // Set filter text to new label for immediate display before props update\n setFilterText(newSelectedItem?.label ?? '');\n };\n\n const handleInputChange = (inputVal: string) => {\n setFilterText(inputVal);\n if (inputVal === '' && value !== null) {\n onChange(null);\n }\n };\n\n const handleOpenChange = (open: boolean, trigger?: 'focus' | 'input' | 'manual') => {\n setIsOpen(open);\n // Initialize filter text to selected label when opening via focus\n if (open && trigger === 'focus') {\n setFilterText(selectedItem?.label ?? '');\n }\n };\n\n return (\n <div className=\"position-relative\">\n {name && <input name={name} type=\"hidden\" value={value ?? ''} />}\n\n <AriaComboBox\n {...props}\n selectedKey={value}\n inputValue={inputValue}\n isDisabled={disabled}\n isInvalid={!!errorMessage}\n menuTrigger=\"focus\"\n allowsEmptyCollection\n onSelectionChange={handleSelectionChange}\n onInputChange={handleInputChange}\n onOpenChange={handleOpenChange}\n >\n {label && <Label className=\"form-label\">{label}</Label>}\n\n <Group\n className={clsx(\n 'form-control d-flex align-items-center gap-1',\n disabled && 'bg-body-secondary',\n isOpen && 'border-primary shadow-sm',\n errorMessage && 'is-invalid',\n )}\n style={{ minHeight: '38px', cursor: disabled ? 'not-allowed' : 'text' }}\n >\n <Input\n className=\"border-0 flex-grow-1 bg-transparent\"\n id={id}\n placeholder={placeholder}\n style={{ outline: 'none' }}\n />\n <Button aria-label=\"Show suggestions\" className=\"border-0 bg-transparent p-0 ms-auto\">\n <i\n aria-hidden=\"true\"\n className={clsx('bi', isOpen ? 'bi-chevron-up' : 'bi-chevron-down', 'text-muted')}\n />\n </Button>\n </Group>\n\n {description && (\n <Text className=\"form-text text-muted\" slot=\"description\">\n {description}\n </Text>\n )}\n\n <FieldError className=\"invalid-feedback d-block\">{errorMessage}</FieldError>\n\n <Popover\n className=\"dropdown-menu show py-0 overflow-auto\"\n offset={2}\n style={{ maxHeight: '300px', width: 'var(--trigger-width)' }}\n >\n <ListBox\n className=\"list-unstyled m-0\"\n items={filteredItems}\n renderEmptyState={() => (\n <div className=\"dropdown-item text-muted\">No options found</div>\n )}\n >\n {(item) => (\n <ListBoxItem\n id={item.id}\n className={({ isFocused, isSelected }) =>\n clsx(\n 'dropdown-item d-flex align-items-center gap-2',\n isFocused && 'active',\n isSelected && 'fw-semibold',\n )\n }\n style={{ cursor: 'pointer' }}\n textValue={item.label}\n >\n <span className=\"flex-grow-1\">{renderItem(item)}</span>\n </ListBoxItem>\n )}\n </ListBox>\n </Popover>\n </AriaComboBox>\n </div>\n );\n}\n\n/**\n * Multi-selection combobox with removable tags.\n */\nexport function TagPicker<T = void>({\n items,\n value,\n onChange,\n placeholder = 'Select...',\n disabled = false,\n name,\n id,\n label,\n description,\n errorMessage,\n renderItem = defaultRenderItem,\n renderTag = defaultRenderTag,\n ...props\n}: TagPickerProps<T>) {\n const { contains } = useFilter({ sensitivity: 'base' });\n const [inputValue, setInputValue] = useState('');\n const [isOpen, setIsOpen] = useState(false);\n\n const selectedItems = useMemo(\n () => items.filter((item) => value.includes(item.id)),\n [items, value],\n );\n\n const filteredItems = useMemo(() => {\n if (!inputValue.trim()) return items;\n return items.filter((item) => {\n const searchable = item.searchableText ?? item.label;\n return contains(searchable, inputValue);\n });\n }, [items, inputValue, contains]);\n\n const handleRemoveTag = (keys: Set<Key>) => {\n const newValue = value.filter((v) => !keys.has(v));\n onChange(newValue);\n };\n\n const handleSelect = (key: Key | null) => {\n const itemId = typeof key === 'string' ? key : null;\n if (!itemId) return;\n if (value.includes(itemId)) {\n onChange(value.filter((v) => v !== itemId));\n } else {\n onChange([...value, itemId]);\n }\n setInputValue('');\n };\n\n return (\n <div className=\"position-relative\">\n {name &&\n (selectedItems.length > 0 ? (\n selectedItems.map((item) => (\n <input key={item.id} name={name} type=\"hidden\" value={item.id} />\n ))\n ) : (\n <input name={name} type=\"hidden\" value=\"\" />\n ))}\n\n <AriaComboBox\n {...props}\n inputValue={inputValue}\n isDisabled={disabled}\n isInvalid={!!errorMessage}\n menuTrigger=\"focus\"\n selectedKey={null}\n allowsEmptyCollection\n onInputChange={setInputValue}\n onOpenChange={setIsOpen}\n onSelectionChange={handleSelect}\n >\n {label && <Label className=\"form-label\">{label}</Label>}\n\n <Group\n className={clsx(\n 'form-control d-flex flex-wrap align-items-center gap-1',\n disabled && 'bg-body-secondary',\n isOpen && 'border-primary shadow-sm',\n errorMessage && 'is-invalid',\n )}\n style={{ minHeight: '38px', cursor: disabled ? 'not-allowed' : 'text' }}\n >\n {selectedItems.length > 0 && (\n <TagGroup\n aria-label=\"Selected items\"\n onRemove={!disabled ? handleRemoveTag : undefined}\n >\n <TagList>\n {selectedItems.map((item) => (\n <Tag\n key={item.id}\n id={item.id}\n className=\"d-inline-flex align-items-center\"\n style={{ lineHeight: 1.2 }}\n textValue={item.label}\n >\n {renderTag(item)}\n {!disabled && (\n <Button\n aria-label={`Remove ${item.label}`}\n className=\"btn-close btn-close-sm ms-1 p-0 border-0 bg-transparent\"\n slot=\"remove\"\n style={{ fontSize: '0.6rem' }}\n />\n )}\n </Tag>\n ))}\n </TagList>\n </TagGroup>\n )}\n\n <div className=\"flex-grow-1 d-flex align-items-center\">\n <Input\n className=\"border-0 flex-grow-1 bg-transparent\"\n id={id}\n placeholder={selectedItems.length === 0 ? placeholder : ''}\n style={{ outline: 'none', minWidth: '60px' }}\n />\n <Button aria-label=\"Show suggestions\" className=\"border-0 bg-transparent p-0 ms-auto\">\n <i\n aria-hidden=\"true\"\n className={clsx('bi', isOpen ? 'bi-chevron-up' : 'bi-chevron-down', 'text-muted')}\n />\n </Button>\n </div>\n </Group>\n\n {description && (\n <Text className=\"form-text text-muted\" slot=\"description\">\n {description}\n </Text>\n )}\n\n <FieldError className=\"invalid-feedback d-block\">{errorMessage}</FieldError>\n\n <Popover\n className=\"dropdown-menu show py-0 overflow-auto\"\n offset={2}\n style={{ maxHeight: '300px', width: 'var(--trigger-width)' }}\n >\n <ListBox\n className=\"list-unstyled m-0\"\n items={filteredItems}\n renderEmptyState={() => (\n <div className=\"dropdown-item text-muted\">No options found</div>\n )}\n >\n {(item) => {\n const isSelected = value.includes(item.id);\n return (\n <ListBoxItem\n id={item.id}\n className={({ isFocused }) =>\n clsx('dropdown-item d-flex align-items-center gap-2', isFocused && 'active')\n }\n style={{ cursor: 'pointer' }}\n textValue={item.label}\n >\n <input\n checked={isSelected}\n className=\"form-check-input m-0\"\n tabIndex={-1}\n type=\"checkbox\"\n readOnly\n />\n <div className=\"flex-grow-1\">{renderItem(item)}</div>\n </ListBoxItem>\n );\n }}\n </ListBox>\n </Popover>\n </AriaComboBox>\n </div>\n );\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FilterDropdown.js","sourceRoot":"","sources":["../../src/components/FilterDropdown.tsx"],"names":[],"mappings":";AAAA,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAkB,OAAO,EAAE,MAAM,OAAO,CAAC;AAChD,OAAO,EACL,MAAM,EACN,aAAa,EACb,OAAO,EACP,WAAW,EACX,OAAO,EAEP,SAAS,GACV,MAAM,uBAAuB,CAAC;AAwB/B,SAAS,kBAAkB,CAAC,CAAa,EAAE,CAAa
|
|
1
|
+
{"version":3,"file":"FilterDropdown.js","sourceRoot":"","sources":["../../src/components/FilterDropdown.tsx"],"names":[],"mappings":"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","sourcesContent":["import clsx from 'clsx';\nimport { type ReactNode, useMemo } from 'react';\nimport {\n Button,\n DialogTrigger,\n ListBox,\n ListBoxItem,\n Popover,\n type Selection,\n Separator,\n} from 'react-aria-components';\n\n// This interface isn't very generic.\n// TODO: When this component is more widely used, improve this.\nexport interface FilterItem {\n id: string;\n name: string;\n color?: string;\n}\n\nexport interface FilterDropdownProps {\n label: string;\n items: FilterItem[];\n selectedIds: Set<string>;\n onChange: (selectedIds: Set<string>) => void;\n renderItem?: (item: FilterItem, isSelected: boolean) => ReactNode;\n disabled?: boolean;\n 'aria-label'?: string;\n /** Maximum height of the dropdown in pixels. */\n maxHeight?: number;\n /** Item IDs that should appear at the top of the list in their original order */\n pinnedIds?: Set<string>;\n}\n\nfunction compareItemsByName(a: FilterItem, b: FilterItem) {\n return a.name.localeCompare(b.name, undefined, { numeric: true });\n}\n\nfunction defaultRenderItem(item: FilterItem, _isSelected: boolean) {\n return item.color ? (\n <span className={`badge color-${item.color}`}>{item.name}</span>\n ) : (\n <span>{item.name}</span>\n );\n}\n\n/**\n * A multi-select filter dropdown component using react-aria-components.\n * Displays a button trigger with selection count badge and a dropdown with checkboxes.\n */\nexport function FilterDropdown({\n label,\n items,\n selectedIds,\n onChange,\n renderItem = defaultRenderItem,\n disabled = false,\n 'aria-label': ariaLabel,\n maxHeight,\n pinnedIds,\n}: FilterDropdownProps) {\n const selectedCount = selectedIds.size;\n\n // Sort items alphabetically for display, with pinned items first\n const sortedItems = useMemo(() => {\n if (!pinnedIds || pinnedIds.size === 0) {\n return [...items].sort(compareItemsByName);\n }\n const pinned = items.filter((item) => pinnedIds.has(item.id));\n const rest = items.filter((item) => !pinnedIds.has(item.id)).sort(compareItemsByName);\n return [...pinned, ...rest];\n }, [items, pinnedIds]);\n\n const handleSelectionChange = (selection: Selection) => {\n if (selection === 'all') {\n onChange(new Set(items.map((item) => item.id)));\n } else {\n onChange(new Set(selection as Set<string>));\n }\n };\n\n const handleClear = () => {\n onChange(new Set());\n };\n\n return (\n <DialogTrigger>\n <Button\n aria-label={ariaLabel ?? `Filter by ${label}`}\n className={clsx(\n 'btn btn-sm d-flex align-items-center gap-1',\n selectedCount > 0 ? 'btn-outline-primary' : 'btn-outline-secondary',\n )}\n isDisabled={disabled}\n >\n {label}\n {selectedCount > 0 && (\n <span className=\"badge bg-primary rounded-pill\">{selectedCount}</span>\n )}\n </Button>\n <Popover\n className=\"dropdown-menu show py-0 d-flex flex-column\"\n offset={4}\n placement=\"bottom start\"\n maxHeight={maxHeight}\n style={{ width: '250px' }}\n >\n <div className=\"pt-2 flex-grow-1 overflow-auto\" style={{ minHeight: 0 }}>\n <ListBox\n aria-label={ariaLabel ?? `Filter by ${label}`}\n className=\"list-unstyled m-0\"\n items={sortedItems}\n selectedKeys={selectedIds}\n selectionMode=\"multiple\"\n selectionBehavior=\"toggle\"\n renderEmptyState={() => (\n <div className=\"dropdown-item text-muted\">No items available</div>\n )}\n onSelectionChange={handleSelectionChange}\n >\n {(item) => (\n <ListBoxItem\n id={item.id}\n className={({ isFocused }) =>\n clsx('dropdown-item d-flex align-items-center gap-2', isFocused && 'active')\n }\n style={{ cursor: 'pointer' }}\n textValue={item.name}\n >\n {({ isSelected }) => (\n <>\n <input\n checked={isSelected}\n className=\"form-check-input m-0 flex-shrink-0\"\n tabIndex={-1}\n type=\"checkbox\"\n readOnly\n />\n {renderItem(item, isSelected)}\n </>\n )}\n </ListBoxItem>\n )}\n </ListBox>\n </div>\n {selectedCount > 0 && (\n <>\n <Separator className=\"dropdown-divider mb-0\" />\n <div className=\"px-3 py-1\">\n <button\n type=\"button\"\n className=\"btn btn-sm btn-link p-0 text-decoration-none\"\n onClick={handleClear}\n >\n Clear selection\n </button>\n </div>\n </>\n )}\n </Popover>\n </DialogTrigger>\n );\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MultiSelectColumnFilter.js","sourceRoot":"","sources":["../../src/components/MultiSelectColumnFilter.tsx"],"names":[],"mappings":";AACA,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAkB,OAAO,EAAE,MAAM,OAAO,CAAC;AAChD,OAAO,QAAQ,MAAM,0BAA0B,CAAC;AAEhD,SAAS,uBAAuB,CAAC,EAAE,KAAK,EAAqB
|
|
1
|
+
{"version":3,"file":"MultiSelectColumnFilter.js","sourceRoot":"","sources":["../../src/components/MultiSelectColumnFilter.tsx"],"names":[],"mappings":"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","sourcesContent":["import type { Column } from '@tanstack/table-core';\nimport clsx from 'clsx';\nimport { type ReactNode, useMemo } from 'react';\nimport Dropdown from 'react-bootstrap/Dropdown';\n\nfunction defaultRenderValueLabel({ value }: { value: string }) {\n return <span>{value}</span>;\n}\n\n/**\n * A component that allows the user to filter a column containing arrays of values.\n * Uses AND logic: rows must contain ALL selected values to match.\n *\n * The filter options (`allColumnValues`) are strings (or string subtypes like\n * enums). The column's `filterFn` is responsible for mapping these string\n * values to the actual column data.\n *\n * @param params\n * @param params.column - The TanStack Table column object\n * @param params.allColumnValues - The string values to display as filter options\n * @param params.renderValueLabel - A function that renders the label for a value\n */\nexport function MultiSelectColumnFilter<TData, TValue extends string = string>({\n column,\n allColumnValues,\n renderValueLabel = defaultRenderValueLabel,\n}: {\n column: Column<TData, unknown>;\n allColumnValues: TValue[];\n renderValueLabel?: (props: { value: TValue; isSelected: boolean }) => ReactNode;\n}) {\n const columnId = column.id;\n\n const label =\n column.columnDef.meta?.label ??\n (typeof column.columnDef.header === 'string' ? column.columnDef.header : column.id);\n\n const columnValuesFilter = column.getFilterValue() as TValue[] | undefined;\n\n const selected = useMemo(() => {\n return new Set(columnValuesFilter);\n }, [columnValuesFilter]);\n\n const toggleSelected = (value: TValue) => {\n const set = new Set(selected);\n if (set.has(value)) {\n set.delete(value);\n } else {\n set.add(value);\n }\n const newValue = Array.from(set);\n column.setFilterValue(newValue);\n };\n\n const hasActiveFilter = selected.size > 0;\n\n return (\n <Dropdown align=\"end\">\n <Dropdown.Toggle\n variant=\"link\"\n className=\"text-muted p-0\"\n id={`filter-${columnId}`}\n aria-label={`Filter ${label.toLowerCase()}`}\n title={`Filter ${label.toLowerCase()}`}\n >\n <i\n className={clsx('bi', hasActiveFilter ? ['bi-funnel-fill', 'text-primary'] : 'bi-funnel')}\n aria-hidden=\"true\"\n />\n </Dropdown.Toggle>\n <Dropdown.Menu className=\"p-0\">\n <div className=\"p-3 pb-0\" style={{ minWidth: '250px' }}>\n <div className=\"d-flex align-items-center justify-content-between mb-2\">\n <div className=\"fw-semibold\">{label}</div>\n <button\n type=\"button\"\n className=\"btn btn-link btn-sm text-decoration-none p-0\"\n onClick={() => column.setFilterValue([])}\n >\n Clear\n </button>\n </div>\n </div>\n\n <div\n className=\"list-group list-group-flush\"\n style={\n {\n // This is needed to prevent the last item's background from covering\n // the dropdown's border radius.\n '--bs-list-group-bg': 'transparent',\n } as React.CSSProperties\n }\n >\n {allColumnValues.map((value) => {\n const isSelected = selected.has(value);\n return (\n <div key={value} className=\"list-group-item d-flex align-items-center gap-3\">\n <div className=\"form-check\">\n <input\n className=\"form-check-input\"\n type=\"checkbox\"\n checked={isSelected}\n id={`${columnId}-${value}`}\n onChange={() => toggleSelected(value)}\n />\n <label className=\"form-check-label fw-normal\" htmlFor={`${columnId}-${value}`}>\n {renderValueLabel({\n value,\n isSelected,\n })}\n </label>\n </div>\n </div>\n );\n })}\n </div>\n </Dropdown.Menu>\n </Dropdown>\n );\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NumericInputColumnFilter.js","sourceRoot":"","sources":["../../src/components/NumericInputColumnFilter.tsx"],"names":[],"mappings":"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","sourcesContent":["import type { Column } from '@tanstack/table-core';\nimport clsx from 'clsx';\nimport Dropdown from 'react-bootstrap/Dropdown';\n\nexport type NumericColumnFilterValue =\n | {\n filterValue: string;\n emptyOnly: false;\n }\n | {\n filterValue: '';\n emptyOnly: true;\n };\n\n/**\n * A component that allows the user to filter a numeric column using comparison operators.\n * Supports syntax like: <1, >0, <=5, >=10, =5, or just 5 (implicit equals)\n *\n * @param params\n * @param params.column - The TanStack Table column object\n */\nexport function NumericInputColumnFilter<TData, TValue>({\n column,\n}: {\n column: Column<TData, TValue>;\n}) {\n const columnId = column.id;\n const value = (column.getFilterValue() as NumericColumnFilterValue | undefined) ?? {\n filterValue: '',\n emptyOnly: false,\n };\n\n const label =\n column.columnDef.meta?.label ??\n (typeof column.columnDef.header === 'string' ? column.columnDef.header : column.id);\n\n const filterValue = value.filterValue;\n const emptyOnly = value.emptyOnly;\n const hasActiveFilter = filterValue.trim().length > 0 || emptyOnly;\n const isInvalid = filterValue.trim().length > 0 && parseNumericFilter(filterValue) === null;\n\n return (\n <Dropdown align=\"end\">\n <Dropdown.Toggle\n variant=\"link\"\n className={clsx(\n 'text-muted p-0',\n hasActiveFilter && (isInvalid ? 'text-warning' : 'text-primary'),\n )}\n id={`filter-${columnId}`}\n aria-label={`Filter ${label.toLowerCase()}`}\n title={`Filter ${label.toLowerCase()}`}\n >\n <i\n className={clsx(\n 'bi',\n isInvalid\n ? 'bi-exclamation-triangle'\n : hasActiveFilter\n ? 'bi-funnel-fill'\n : 'bi-funnel',\n )}\n aria-hidden=\"true\"\n />\n </Dropdown.Toggle>\n <Dropdown.Menu className=\"p-0\">\n <div className=\"p-3\" style={{ minWidth: '240px' }}>\n <div className=\"d-flex align-items-center justify-content-between mb-2\">\n <label className=\"form-label fw-semibold mb-0\" id={`${columnId}-filter-label`}>\n {label}\n </label>\n <button\n type=\"button\"\n className={clsx(\n 'btn btn-link btn-sm text-decoration-none',\n !hasActiveFilter && 'invisible',\n )}\n onClick={() => {\n column.setFilterValue({ filterValue: '', emptyOnly: false });\n }}\n >\n Clear\n </button>\n </div>\n <input\n type=\"text\"\n className={clsx('form-control form-control-sm', isInvalid && 'is-invalid')}\n placeholder=\"e.g., >0, <5, =10\"\n aria-labelledby={`${columnId}-filter-label`}\n value={filterValue}\n disabled={emptyOnly}\n aria-describedby={`${columnId}-filter-description`}\n onInput={(e) => {\n column.setFilterValue({\n filterValue: e.currentTarget.value,\n emptyOnly: false,\n });\n }}\n onClick={(e) => e.stopPropagation()}\n />\n {isInvalid && (\n <div className=\"invalid-feedback d-block\">\n Invalid filter format. Use operators like <code>>5</code> or <code><=10</code>\n </div>\n )}\n {!isInvalid && (\n <small className=\"form-text text-nowrap\" id={`${columnId}-filter-description`}>\n Operators: <code><</code>, <code>></code>, <code><=</code>,{' '}\n <code>>=</code>, <code>=</code>\n </small>\n )}\n <div className=\"form-check mt-2\">\n <input\n className=\"form-check-input\"\n type=\"checkbox\"\n checked={emptyOnly}\n id={`${columnId}-empty-filter`}\n onChange={(e) => {\n column.setFilterValue(\n e.currentTarget.checked\n ? { filterValue: '', emptyOnly: true }\n : { filterValue: '', emptyOnly: false },\n );\n }}\n />\n <label className=\"form-check-label\" htmlFor={`${columnId}-empty-filter`}>\n Empty values\n </label>\n </div>\n </div>\n </Dropdown.Menu>\n </Dropdown>\n );\n}\n\n/**\n * Helper function to parse a numeric filter value.\n * Returns null if the filter is invalid or empty.\n *\n * @param filterValue - The filter string (e.g., \">5\", \"<=10\", \"3\")\n * @returns Parsed operator and value, or null if invalid\n */\nexport function parseNumericFilter(filterValue: string): {\n operator: '<' | '>' | '<=' | '>=' | '=';\n value: number;\n} | null {\n if (!filterValue.trim()) return null;\n\n const match = filterValue.trim().match(/^(<=?|>=?|=)?\\s*(-?\\d+\\.?\\d*)$/);\n if (!match) return null;\n\n const operator = (match[1] || '=') as '<' | '>' | '<=' | '>=' | '=';\n const value = Number.parseFloat(match[2]);\n\n if (Number.isNaN(value)) return null;\n\n return { operator, value };\n}\n\n/**\n * TanStack Table filter function for numeric columns.\n * Use this as the `filterFn` for numeric columns.\n *\n * @example\n * {\n * id: 'manual_points',\n * accessorKey: 'manual_points',\n * filterFn: numericColumnFilterFn,\n * }\n */\nexport function numericColumnFilterFn(\n row: any,\n columnId: string,\n { filterValue, emptyOnly }: NumericColumnFilterValue,\n): boolean {\n // Handle object-based filter value\n const cellValue = row.getValue(columnId) as number | null;\n const isEmpty = cellValue == null;\n\n if (emptyOnly) {\n return isEmpty;\n }\n\n // If there's no numeric filter, show all rows\n const parsed = parseNumericFilter(filterValue);\n if (!parsed) return true;\n\n // If cell is empty and we're doing numeric filtering, don't show it\n if (isEmpty) return false;\n\n // Apply numeric filter\n switch (parsed.operator) {\n case '<':\n return cellValue < parsed.value;\n case '>':\n return cellValue > parsed.value;\n case '<=':\n return cellValue <= parsed.value;\n case '>=':\n return cellValue >= parsed.value;\n case '=':\n return cellValue === parsed.value;\n default:\n return true;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"NumericInputColumnFilter.js","sourceRoot":"","sources":["../../src/components/NumericInputColumnFilter.tsx"],"names":[],"mappings":"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","sourcesContent":["import type { Column } from '@tanstack/table-core';\nimport clsx from 'clsx';\nimport Dropdown from 'react-bootstrap/Dropdown';\n\nexport type NumericColumnFilterValue =\n | {\n filterValue: string;\n emptyOnly: false;\n }\n | {\n filterValue: '';\n emptyOnly: true;\n };\n\n/**\n * A component that allows the user to filter a numeric column using comparison operators.\n * Supports syntax like: <1, >0, <=5, >=10, =5, or just 5 (implicit equals)\n *\n * @param params\n * @param params.column - The TanStack Table column object\n */\nexport function NumericInputColumnFilter<TData, TValue>({\n column,\n}: {\n column: Column<TData, TValue>;\n}) {\n const columnId = column.id;\n const value = (column.getFilterValue() as NumericColumnFilterValue | undefined) ?? {\n filterValue: '',\n emptyOnly: false,\n };\n\n const label =\n column.columnDef.meta?.label ??\n (typeof column.columnDef.header === 'string' ? column.columnDef.header : column.id);\n\n const filterValue = value.filterValue;\n const emptyOnly = value.emptyOnly;\n const hasActiveFilter = filterValue.trim().length > 0 || emptyOnly;\n const isInvalid = filterValue.trim().length > 0 && parseNumericFilter(filterValue) === null;\n\n return (\n <Dropdown align=\"end\">\n <Dropdown.Toggle\n variant=\"link\"\n className={clsx(\n 'text-muted p-0',\n hasActiveFilter && (isInvalid ? 'text-warning' : 'text-primary'),\n )}\n id={`filter-${columnId}`}\n aria-label={`Filter ${label.toLowerCase()}`}\n title={`Filter ${label.toLowerCase()}`}\n >\n <i\n className={clsx(\n 'bi',\n isInvalid\n ? 'bi-exclamation-triangle'\n : hasActiveFilter\n ? 'bi-funnel-fill'\n : 'bi-funnel',\n )}\n aria-hidden=\"true\"\n />\n </Dropdown.Toggle>\n <Dropdown.Menu className=\"p-0\">\n <div className=\"p-3\" style={{ minWidth: '240px' }}>\n <div className=\"d-flex align-items-center justify-content-between mb-2\">\n <label className=\"form-label fw-semibold mb-0\" id={`${columnId}-filter-label`}>\n {label}\n </label>\n <button\n type=\"button\"\n className={clsx(\n 'btn btn-link btn-sm text-decoration-none',\n !hasActiveFilter && 'invisible',\n )}\n onClick={() => {\n column.setFilterValue({ filterValue: '', emptyOnly: false });\n }}\n >\n Clear\n </button>\n </div>\n <input\n type=\"text\"\n className={clsx('form-control form-control-sm', isInvalid && 'is-invalid')}\n placeholder=\"e.g., >0, <5, =10\"\n aria-labelledby={`${columnId}-filter-label`}\n value={filterValue}\n disabled={emptyOnly}\n aria-describedby={`${columnId}-filter-description`}\n onInput={(e) => {\n column.setFilterValue({\n filterValue: e.currentTarget.value,\n emptyOnly: false,\n });\n }}\n onClick={(e) => e.stopPropagation()}\n />\n {isInvalid && (\n <div className=\"invalid-feedback d-block\">\n Invalid filter format. Use operators like <code>>5</code> or <code><=10</code>\n </div>\n )}\n {!isInvalid && (\n <small className=\"form-text text-nowrap\" id={`${columnId}-filter-description`}>\n Operators: <code><</code>, <code>></code>, <code><=</code>,{' '}\n <code>>=</code>, <code>=</code>\n </small>\n )}\n <div className=\"form-check mt-2\">\n <input\n className=\"form-check-input\"\n type=\"checkbox\"\n checked={emptyOnly}\n id={`${columnId}-empty-filter`}\n onChange={(e) => {\n column.setFilterValue(\n e.currentTarget.checked\n ? { filterValue: '', emptyOnly: true }\n : { filterValue: '', emptyOnly: false },\n );\n }}\n />\n <label className=\"form-check-label\" htmlFor={`${columnId}-empty-filter`}>\n Empty values\n </label>\n </div>\n </div>\n </Dropdown.Menu>\n </Dropdown>\n );\n}\n\n/**\n * Helper function to parse a numeric filter value.\n * Returns null if the filter is invalid or empty.\n *\n * @param filterValue - The filter string (e.g., \">5\", \"<=10\", \"3\")\n * @returns Parsed operator and value, or null if invalid\n */\nexport function parseNumericFilter(filterValue: string): {\n operator: '<' | '>' | '<=' | '>=' | '=';\n value: number;\n} | null {\n if (!filterValue.trim()) return null;\n\n const match = filterValue.trim().match(/^(<=?|>=?|=)?\\s*(-?\\d+\\.?\\d*)$/);\n if (!match) return null;\n\n const operator = (match[1] || '=') as '<' | '>' | '<=' | '>=' | '=';\n const value = Number.parseFloat(match[2]);\n\n if (Number.isNaN(value)) return null;\n\n return { operator, value };\n}\n\n/**\n * TanStack Table filter function for numeric columns.\n * Use this as the `filterFn` for numeric columns.\n *\n * @example\n * {\n * id: 'manual_points',\n * accessorKey: 'manual_points',\n * filterFn: numericColumnFilterFn,\n * }\n */\nexport function numericColumnFilterFn(\n row: any,\n columnId: string,\n { filterValue, emptyOnly }: NumericColumnFilterValue,\n): boolean {\n // Handle object-based filter value\n const cellValue = row.getValue(columnId) as number | null;\n const isEmpty = cellValue == null;\n\n if (emptyOnly) {\n return isEmpty;\n }\n\n // If there's no numeric filter, show all rows\n const parsed = parseNumericFilter(filterValue);\n if (!parsed) return true;\n\n // If cell is empty and we're doing numeric filtering, don't show it\n if (isEmpty) return false;\n\n // Apply numeric filter\n switch (parsed.operator) {\n case '<':\n return cellValue < parsed.value;\n case '>':\n return cellValue > parsed.value;\n case '<=':\n return cellValue <= parsed.value;\n case '>=':\n return cellValue >= parsed.value;\n case '=':\n return cellValue === parsed.value;\n default:\n return true;\n }\n}\n"]}
|