From 42f87231f35249eb9eca09943797813c7faa6942 Mon Sep 17 00:00:00 2001 From: KJ Monahan Date: Mon, 1 Apr 2024 14:10:51 -0500 Subject: [PATCH 1/2] Initial work on sortable table --- source/03-components/Table/Table.tsx | 49 +++++ source/03-components/Table/table.yml | 316 +++++++++++++++++++++++++++ 2 files changed, 365 insertions(+) create mode 100644 source/03-components/Table/Table.tsx create mode 100644 source/03-components/Table/table.yml diff --git a/source/03-components/Table/Table.tsx b/source/03-components/Table/Table.tsx new file mode 100644 index 00000000..f17805e1 --- /dev/null +++ b/source/03-components/Table/Table.tsx @@ -0,0 +1,49 @@ +import clsx from 'clsx'; +import { GessoComponent } from 'gesso'; +import { + DetailedReactHTMLElement, + HTMLAttributes, + JSX, + ReactNode, +} from 'react'; + +type TableCell = + | DetailedReactHTMLElement< + HTMLAttributes, + HTMLTableHeaderCellElement + > + | DetailedReactHTMLElement< + HTMLAttributes, + HTMLTableDataCellElement + >; + +interface TableProps extends GessoComponent { + isScrollable?: boolean; + isSortable?: boolean; + caption?: ReactNode; + header?: TableCell[]; + footer?: TableCell[]; +} + +function Table({ + caption, + header, + footer, + modifierClasses, +}: TableProps): JSX.Element { + return ( + + {caption && } + {header ? ( + + {header.map(cell => cell)} + + ) : null} + {footer ? ( + + {footer.map(cell => cell)} + + ) : null} +
{caption}
+ ); +} diff --git a/source/03-components/Table/table.yml b/source/03-components/Table/table.yml new file mode 100644 index 00000000..9d7e4b64 --- /dev/null +++ b/source/03-components/Table/table.yml @@ -0,0 +1,316 @@ +modifierClasses: '' +caption: 'Table Caption' +isScrollable: false +isSortable: false +header: + - + attributes: ' scope="col"' + content: 'City' + - + attributes: ' scope="col" data-sortable' + content: 'Country' + - + attributes: ' scope="col" data-sortable' + content: 'Population' +footer: false +rows: + - + attributes: '' + cells: + - + tag: 'td' + attributes: '' + content: 'Tokyo' + - + tag: 'td' + attributes: '' + content: 'Japan' + - + tag: 'td' + attributes: '' + content: '37,468,000' + - + attributes: '' + cells: + - + tag: 'td' + attributes: '' + content: 'Delhi' + - + tag: 'td' + attributes: '' + content: 'India' + - + tag: 'td' + attributes: '' + content: '28,514,000' + - + attributes: '' + cells: + - + tag: 'td' + attributes: '' + content: 'Shanghai' + - + tag: 'td' + attributes: '' + content: 'China' + - + tag: 'td' + attributes: '' + content: '25,582,000' + - + attributes: '' + cells: + - + tag: 'td' + attributes: '' + content: 'São Paulo' + - + tag: 'td' + attributes: '' + content: 'Brazil' + - + tag: 'td' + attributes: '' + content: '21,650,000' + - + attributes: '' + cells: + - + tag: 'td' + attributes: '' + content: 'Mexico City' + - + tag: 'td' + attributes: '' + content: 'Mexico' + - + tag: 'td' + attributes: '' + content: '21,581,000' + - + attributes: '' + cells: + - + tag: 'td' + attributes: '' + content: 'Cairo' + - + tag: 'td' + attributes: '' + content: 'Egypt' + - + tag: 'td' + attributes: '' + content: '20,076,000' + - + attributes: '' + cells: + - + tag: 'td' + attributes: '' + content: 'Mumbai' + - + tag: 'td' + attributes: '' + content: 'India' + - + tag: 'td' + attributes: '' + content: '19,980,000' + - + attributes: '' + cells: + - + tag: 'td' + attributes: '' + content: 'Beijing' + - + tag: 'td' + attributes: '' + content: 'China' + - + tag: 'td' + attributes: '' + content: '19,618,000' + - + attributes: '' + cells: + - + tag: 'td' + attributes: '' + content: 'Dhaka' + - + tag: 'td' + attributes: '' + content: 'Bangladesh' + - + tag: 'td' + attributes: '' + content: '19,578,000' + - + attributes: '' + cells: + - + tag: 'td' + attributes: '' + content: 'Osaka' + - + tag: 'td' + attributes: '' + content: 'Japan' + - + tag: 'td' + attributes: '' + content: '19,281,000' + - + attributes: '' + cells: + - + tag: 'td' + attributes: '' + content: 'New York' + - + tag: 'td' + attributes: '' + content: 'United States' + - + tag: 'td' + attributes: '' + content: '18,819,000' + - + attributes: '' + cells: + - + tag: 'td' + attributes: '' + content: 'Karachi' + - + tag: 'td' + attributes: '' + content: 'Pakistan' + - + tag: 'td' + attributes: '' + content: '15,400,000' + - + attributes: '' + cells: + - + tag: 'td' + attributes: '' + content: 'Buenos Aires' + - + tag: 'td' + attributes: '' + content: 'Argentina' + - + tag: 'td' + attributes: '' + content: '14,967,000' + - + attributes: '' + cells: + - + tag: 'td' + attributes: '' + content: 'Chongqing' + - + tag: 'td' + attributes: '' + content: 'China' + - + tag: 'td' + attributes: '' + content: '14,838,000' + - + attributes: '' + cells: + - + tag: 'td' + attributes: '' + content: 'Istanbul' + - + tag: 'td' + attributes: '' + content: 'Turkey' + - + tag: 'td' + attributes: '' + content: '14,751,000' + - + attributes: '' + cells: + - + tag: 'td' + attributes: '' + content: 'Kolkata' + - + tag: 'td' + attributes: '' + content: 'India' + - + tag: 'td' + attributes: '' + content: '14,681,000' + - + attributes: '' + cells: + - + tag: 'td' + attributes: '' + content: 'Manila' + - + tag: 'td' + attributes: '' + content: 'Philippines' + - + tag: 'td' + attributes: '' + content: '13,482,000' + - + attributes: '' + cells: + - + tag: 'td' + attributes: '' + content: 'Lagos' + - + tag: 'td' + attributes: '' + content: 'Nigeria' + - + tag: 'td' + attributes: '' + content: '13,463,000' + - + attributes: '' + cells: + - + tag: 'td' + attributes: '' + content: 'Rio de Janeiro' + - + tag: 'td' + attributes: '' + content: 'Brazil' + - + tag: 'td' + attributes: '' + content: '13,293,000' + - + attributes: '' + cells: + - + tag: 'td' + attributes: '' + content: 'Tianjin' + - + tag: 'td' + attributes: '' + content: 'China' + - + tag: 'td' + attributes: '' + content: '13,215,000' From 34caf7e37a7ab4793e8a4789b3e2b833fdbb7ced Mon Sep 17 00:00:00 2001 From: KJ Monahan Date: Mon, 8 Apr 2024 11:13:28 -0500 Subject: [PATCH 2/2] Port sortable table styles and add an example --- .storybook/decorators.jsx | 9 + source/00-config/index.css | 1 + source/00-config/vars/colors.css | 3 +- source/00-config/vars/dynamic.css | 3 + source/01-global/icon/icons/Sort.tsx | 37 ++ source/01-global/icon/icons/Sorted.tsx | 37 ++ source/01-global/icon/icons/index.tsx | 6 + source/01-global/icon/svgs/sort.svg | 1 + source/01-global/icon/svgs/sorted.svg | 1 + .../SiteContainer/SiteContainer.tsx | 9 +- source/03-components/Table/SortableHeader.tsx | 48 +++ source/03-components/Table/Table.stories.tsx | 189 +++++++++++ source/03-components/Table/Table.tsx | 49 --- source/03-components/Table/table.data.yml | 61 ++++ source/03-components/Table/table.module.css | 60 ++++ source/03-components/Table/table.yml | 316 ------------------ source/06-utility/setScrollbarProperty.ts | 30 ++ 17 files changed, 493 insertions(+), 367 deletions(-) create mode 100644 .storybook/decorators.jsx create mode 100644 source/00-config/vars/dynamic.css create mode 100644 source/01-global/icon/icons/Sort.tsx create mode 100644 source/01-global/icon/icons/Sorted.tsx create mode 100644 source/01-global/icon/svgs/sort.svg create mode 100644 source/01-global/icon/svgs/sorted.svg create mode 100644 source/03-components/Table/SortableHeader.tsx create mode 100644 source/03-components/Table/Table.stories.tsx delete mode 100644 source/03-components/Table/Table.tsx create mode 100644 source/03-components/Table/table.data.yml create mode 100644 source/03-components/Table/table.module.css delete mode 100644 source/03-components/Table/table.yml create mode 100644 source/06-utility/setScrollbarProperty.ts diff --git a/.storybook/decorators.jsx b/.storybook/decorators.jsx new file mode 100644 index 00000000..835adba4 --- /dev/null +++ b/.storybook/decorators.jsx @@ -0,0 +1,9 @@ +import Constrain from '../source/02-layouts/Constrain/Constrain'; + +const withGlobalWrapper = Story => ( + + + +); + +export { withGlobalWrapper }; diff --git a/source/00-config/index.css b/source/00-config/index.css index bfa6cb8f..23b92162 100644 --- a/source/00-config/index.css +++ b/source/00-config/index.css @@ -12,5 +12,6 @@ @layer config.usage { @import 'vars/colors.css'; + @import 'vars/dynamic.css'; @import 'vars/form.css'; } diff --git a/source/00-config/vars/colors.css b/source/00-config/vars/colors.css index 3910ab9c..686b87d8 100644 --- a/source/00-config/vars/colors.css +++ b/source/00-config/vars/colors.css @@ -21,8 +21,9 @@ --selection-text: var(--grayscale-white); --table-background: var(--grayscale-white); - --tablebackground-foot: var(--grayscale-gray-1); + --table-background-foot: var(--grayscale-gray-1); --table-background-head: var(--grayscale-gray-1); + --table-background-sorted: var(--brand-blue-light-2); --table-border: var(--grayscale-gray-5); --text-link: var(--brand-blue-base); diff --git a/source/00-config/vars/dynamic.css b/source/00-config/vars/dynamic.css new file mode 100644 index 00000000..7003f2c8 --- /dev/null +++ b/source/00-config/vars/dynamic.css @@ -0,0 +1,3 @@ +:root { + --scrollbar-width: 0px; +} diff --git a/source/01-global/icon/icons/Sort.tsx b/source/01-global/icon/icons/Sort.tsx new file mode 100644 index 00000000..29e2d930 --- /dev/null +++ b/source/01-global/icon/icons/Sort.tsx @@ -0,0 +1,37 @@ +// organize-imports-ignore +// This component is automatically generated. +// SVGs should be added to icon/svgs. +// See the project documentation for more information. +// tslint:disable:ordered-imports +import clsx from 'clsx'; +import * as React from 'react'; +import type { SVGProps } from 'react'; +interface SVGRProps { + title?: string; + titleId?: string; + isHidden?: boolean; + modifierClasses?: string | string[]; +} +const SvgSort = ({ + modifierClasses, + isHidden, + title, + titleId, + ...props +}: SVGProps & SVGRProps) => { + return ( + + {title ? {title} : null} + + + ); +}; +export default SvgSort; diff --git a/source/01-global/icon/icons/Sorted.tsx b/source/01-global/icon/icons/Sorted.tsx new file mode 100644 index 00000000..aa0cf55f --- /dev/null +++ b/source/01-global/icon/icons/Sorted.tsx @@ -0,0 +1,37 @@ +// organize-imports-ignore +// This component is automatically generated. +// SVGs should be added to icon/svgs. +// See the project documentation for more information. +// tslint:disable:ordered-imports +import clsx from 'clsx'; +import * as React from 'react'; +import type { SVGProps } from 'react'; +interface SVGRProps { + title?: string; + titleId?: string; + isHidden?: boolean; + modifierClasses?: string | string[]; +} +const SvgSorted = ({ + modifierClasses, + isHidden, + title, + titleId, + ...props +}: SVGProps & SVGRProps) => { + return ( + + {title ? {title} : null} + + + ); +}; +export default SvgSorted; diff --git a/source/01-global/icon/icons/index.tsx b/source/01-global/icon/icons/index.tsx index cf40248b..808f4159 100644 --- a/source/01-global/icon/icons/index.tsx +++ b/source/01-global/icon/icons/index.tsx @@ -31,6 +31,12 @@ const Icons = { Rss: dynamic(() => import('./Rss'), { loading: () => , }), + Sort: dynamic(() => import('./Sort'), { + loading: () => , + }), + Sorted: dynamic(() => import('./Sorted'), { + loading: () => , + }), Twitter: dynamic(() => import('./Twitter'), { loading: () => , }), diff --git a/source/01-global/icon/svgs/sort.svg b/source/01-global/icon/svgs/sort.svg new file mode 100644 index 00000000..dbbb5615 --- /dev/null +++ b/source/01-global/icon/svgs/sort.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/source/01-global/icon/svgs/sorted.svg b/source/01-global/icon/svgs/sorted.svg new file mode 100644 index 00000000..fe045879 --- /dev/null +++ b/source/01-global/icon/svgs/sorted.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/source/02-layouts/SiteContainer/SiteContainer.tsx b/source/02-layouts/SiteContainer/SiteContainer.tsx index 820c73a3..92453285 100644 --- a/source/02-layouts/SiteContainer/SiteContainer.tsx +++ b/source/02-layouts/SiteContainer/SiteContainer.tsx @@ -1,5 +1,8 @@ +'use client'; + import { GessoComponent } from 'gesso'; -import { ReactNode } from 'react'; +import { JSX, ReactNode, useEffect } from 'react'; +import setScrollbarProperty from '../../06-utility/setScrollbarProperty'; import styles from './site-container.module.css'; interface SiteContainerProps extends GessoComponent { @@ -7,6 +10,10 @@ interface SiteContainerProps extends GessoComponent { } function SiteContainer({ children }: SiteContainerProps): JSX.Element { + useEffect(() => { + setScrollbarProperty(); + }, []); + return
{children}
; } diff --git a/source/03-components/Table/SortableHeader.tsx b/source/03-components/Table/SortableHeader.tsx new file mode 100644 index 00000000..c0b1d8bb --- /dev/null +++ b/source/03-components/Table/SortableHeader.tsx @@ -0,0 +1,48 @@ +import { JSX, MouseEvent } from 'react'; +import Sort from '../../01-global/icon/icons/Sort'; +import Sorted from '../../01-global/icon/icons/Sorted'; +import styles from './table.module.css'; + +interface SortableHeaderProps { + column: string; + label: string; + direction?: 'ascending' | 'descending'; + updateSort: (newSortColumn: string) => void; +} + +function SortableHeader({ + column, + label, + direction, + updateSort, +}: SortableHeaderProps): JSX.Element { + const Icon = direction ? Sorted : Sort; + + const handleClick = (e: MouseEvent) => { + e.preventDefault(); + updateSort(column); + }; + + return ( + +
+ {label} + +
+ + ); +} + +export default SortableHeader; diff --git a/source/03-components/Table/Table.stories.tsx b/source/03-components/Table/Table.stories.tsx new file mode 100644 index 00000000..9727a079 --- /dev/null +++ b/source/03-components/Table/Table.stories.tsx @@ -0,0 +1,189 @@ +import { Meta, StoryObj } from '@storybook/react'; +import clsx from 'clsx'; +import { useMemo, useState } from 'react'; +import { withGlobalWrapper } from '../../../.storybook/decorators'; +import SiteContainer from '../../02-layouts/SiteContainer/SiteContainer'; +import SortableHeader from './SortableHeader'; +import tableData from './table.data.yml'; +import styles from './table.module.css'; + +type ExampleData = { + city: string; + country: string; + population: number; +}; + +const settings: Meta<{ + isScrollable?: boolean; + caption?: string; + data: ExampleData[]; +}> = { + title: 'Components/Table', + decorators: [ + Story => ( + + + + ), + withGlobalWrapper, + ], + args: { + isScrollable: false, + caption: 'Table caption', + }, + argTypes: { + isScrollable: { + type: 'boolean', + }, + caption: { + type: 'string', + }, + }, + parameters: { + controls: { + include: ['isScrollable', 'caption'], + }, + }, + tags: ['autodocs'], +}; + +type Story = StoryObj<{ + isScrollable?: boolean; + caption?: string; + data: ExampleData[]; +}>; + +const Default: Story = { + render: ({ isScrollable, caption, data }) => { + return ( + + {caption && } + + + + + + + + + {data.map(row => ( + + + + + + ))} + +
{caption}
CityCountryPopulation
{row.city}{row.country}{Number(row.population).toLocaleString('en-US')}
+ ); + }, + args: { + ...tableData, + }, +}; + +const Sortable: Story = { + render: function SortableTable({ isScrollable, caption, data }) { + const [sortColumn, setSortColumn] = useState< + 'city' | 'country' | 'population' | undefined + >(undefined); + const [sortDirection, setSortDirection] = useState< + 'ascending' | 'descending' + >('ascending'); + const sortedData = useMemo( + () => + sortColumn + ? [...data].sort((thisRow, nextRow) => { + const cellA = + sortDirection === 'ascending' + ? thisRow[sortColumn] + : nextRow[sortColumn]; + const cellB = + sortDirection === 'ascending' + ? nextRow[sortColumn] + : thisRow[sortColumn]; + if (typeof cellA === 'number' && typeof cellB === 'number') { + return cellA - cellB; + } + return cellA.toString().localeCompare(cellB.toString()); + }) + : data, + [data, sortColumn, sortDirection], + ); + + const updateSort = (newSortColumn: string) => { + if (sortColumn === newSortColumn) { + setSortDirection(prevState => + prevState === 'ascending' ? 'descending' : 'ascending', + ); + } else { + setSortColumn(newSortColumn as 'city' | 'country' | 'population'); + setSortDirection('ascending'); + } + }; + + return ( + <> + + {caption && } + + + + + + + + + {sortedData.map(row => ( + + + + + + ))} + +
{caption}
{row.city}{row.country}{Number(row.population).toLocaleString('en-US')}
+
+ {sortColumn + ? `The table ${caption && `named "${caption}"`} is now sorted by + ${sortColumn} in ${sortDirection} order.` + : ''} +
+ + ); + }, + args: { + ...tableData, + }, +}; + +export default settings; +export { Default, Sortable }; diff --git a/source/03-components/Table/Table.tsx b/source/03-components/Table/Table.tsx deleted file mode 100644 index f17805e1..00000000 --- a/source/03-components/Table/Table.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import clsx from 'clsx'; -import { GessoComponent } from 'gesso'; -import { - DetailedReactHTMLElement, - HTMLAttributes, - JSX, - ReactNode, -} from 'react'; - -type TableCell = - | DetailedReactHTMLElement< - HTMLAttributes, - HTMLTableHeaderCellElement - > - | DetailedReactHTMLElement< - HTMLAttributes, - HTMLTableDataCellElement - >; - -interface TableProps extends GessoComponent { - isScrollable?: boolean; - isSortable?: boolean; - caption?: ReactNode; - header?: TableCell[]; - footer?: TableCell[]; -} - -function Table({ - caption, - header, - footer, - modifierClasses, -}: TableProps): JSX.Element { - return ( - - {caption && } - {header ? ( - - {header.map(cell => cell)} - - ) : null} - {footer ? ( - - {footer.map(cell => cell)} - - ) : null} -
{caption}
- ); -} diff --git a/source/03-components/Table/table.data.yml b/source/03-components/Table/table.data.yml new file mode 100644 index 00000000..45cc1788 --- /dev/null +++ b/source/03-components/Table/table.data.yml @@ -0,0 +1,61 @@ +data: + - city: 'Tokyo' + country: 'Japan' + population: '37468000' + - city: 'Delhi' + country: 'India' + population: '28514000' + - city: 'Shanghai' + country: 'China' + population: '25582000' + - city: 'São Paulo' + country: 'Brazil' + population: '21650000' + - city: 'Mexico City' + country: 'Mexico' + population: '21581000' + - city: 'Cairo' + country: 'Egypt' + population: '20076000' + - city: 'Mumbai' + country: 'India' + population: '19980000' + - city: 'Beijing' + country: 'China' + population: '19618000' + - city: 'Dhaka' + country: 'Bangladesh' + population: '19578000' + - city: 'Osaka' + country: 'Japan' + population: '19281000' + - city: 'New York' + country: 'United States' + population: '18819000' + - city: 'Karachi' + country: 'Pakistan' + population: '15400000' + - city: 'Buenos Aires' + country: 'Argentina' + population: '14967000' + - city: 'Chongqing' + country: 'China' + population: '14838000' + - city: 'Istanbul' + country: 'Turkey' + population: '14751000' + - city: 'Kolkata' + country: 'India' + population: '14681000' + - city: 'Manila' + country: 'Philippines' + population: '13482000' + - city: 'Lagos' + country: 'Nigeria' + population: '13463000' + - city: 'Rio de Janeiro' + country: 'Brazil' + population: '13293000' + - city: 'Tianjin' + country: 'China' + population: '13215000' diff --git a/source/03-components/Table/table.module.css b/source/03-components/Table/table.module.css new file mode 100644 index 00000000..9fe84413 --- /dev/null +++ b/source/03-components/Table/table.module.css @@ -0,0 +1,60 @@ +@import 'mixins'; + +@layer components { + .is-scrollable { + thead { + display: table; + inline-size: calc(100% - var(--scrollbar-width)); + table-layout: fixed; + } + + tbody { + display: block; + max-block-size: rem-convert(365px); + overflow: hidden auto; + + tr { + display: table; + inline-size: 100%; + table-layout: fixed; + } + } + } + + .is-sortable { + th[data-sortable] { + padding-inline-end: var(--spacing-5); + position: relative; + + &[aria-sort] { + background-color: var(--table-background-sorted); + } + + &[aria-sort='descending'] { + :global(.icon) { + rotate: 180deg; + } + } + } + } + + .header-button { + @include text-button; + + color: inherit; + display: block; + inline-size: var(--spacing-4); + inset-block: var(--spacing-0-5); + inset-inline-end: var(--spacing-0-5); + outline-offset: 0; + position: absolute; + + &:focus-visible { + outline-color: var(--grayscale-white); + } + } + + .announcement-region { + @include visually-hidden; + } +} diff --git a/source/03-components/Table/table.yml b/source/03-components/Table/table.yml deleted file mode 100644 index 9d7e4b64..00000000 --- a/source/03-components/Table/table.yml +++ /dev/null @@ -1,316 +0,0 @@ -modifierClasses: '' -caption: 'Table Caption' -isScrollable: false -isSortable: false -header: - - - attributes: ' scope="col"' - content: 'City' - - - attributes: ' scope="col" data-sortable' - content: 'Country' - - - attributes: ' scope="col" data-sortable' - content: 'Population' -footer: false -rows: - - - attributes: '' - cells: - - - tag: 'td' - attributes: '' - content: 'Tokyo' - - - tag: 'td' - attributes: '' - content: 'Japan' - - - tag: 'td' - attributes: '' - content: '37,468,000' - - - attributes: '' - cells: - - - tag: 'td' - attributes: '' - content: 'Delhi' - - - tag: 'td' - attributes: '' - content: 'India' - - - tag: 'td' - attributes: '' - content: '28,514,000' - - - attributes: '' - cells: - - - tag: 'td' - attributes: '' - content: 'Shanghai' - - - tag: 'td' - attributes: '' - content: 'China' - - - tag: 'td' - attributes: '' - content: '25,582,000' - - - attributes: '' - cells: - - - tag: 'td' - attributes: '' - content: 'São Paulo' - - - tag: 'td' - attributes: '' - content: 'Brazil' - - - tag: 'td' - attributes: '' - content: '21,650,000' - - - attributes: '' - cells: - - - tag: 'td' - attributes: '' - content: 'Mexico City' - - - tag: 'td' - attributes: '' - content: 'Mexico' - - - tag: 'td' - attributes: '' - content: '21,581,000' - - - attributes: '' - cells: - - - tag: 'td' - attributes: '' - content: 'Cairo' - - - tag: 'td' - attributes: '' - content: 'Egypt' - - - tag: 'td' - attributes: '' - content: '20,076,000' - - - attributes: '' - cells: - - - tag: 'td' - attributes: '' - content: 'Mumbai' - - - tag: 'td' - attributes: '' - content: 'India' - - - tag: 'td' - attributes: '' - content: '19,980,000' - - - attributes: '' - cells: - - - tag: 'td' - attributes: '' - content: 'Beijing' - - - tag: 'td' - attributes: '' - content: 'China' - - - tag: 'td' - attributes: '' - content: '19,618,000' - - - attributes: '' - cells: - - - tag: 'td' - attributes: '' - content: 'Dhaka' - - - tag: 'td' - attributes: '' - content: 'Bangladesh' - - - tag: 'td' - attributes: '' - content: '19,578,000' - - - attributes: '' - cells: - - - tag: 'td' - attributes: '' - content: 'Osaka' - - - tag: 'td' - attributes: '' - content: 'Japan' - - - tag: 'td' - attributes: '' - content: '19,281,000' - - - attributes: '' - cells: - - - tag: 'td' - attributes: '' - content: 'New York' - - - tag: 'td' - attributes: '' - content: 'United States' - - - tag: 'td' - attributes: '' - content: '18,819,000' - - - attributes: '' - cells: - - - tag: 'td' - attributes: '' - content: 'Karachi' - - - tag: 'td' - attributes: '' - content: 'Pakistan' - - - tag: 'td' - attributes: '' - content: '15,400,000' - - - attributes: '' - cells: - - - tag: 'td' - attributes: '' - content: 'Buenos Aires' - - - tag: 'td' - attributes: '' - content: 'Argentina' - - - tag: 'td' - attributes: '' - content: '14,967,000' - - - attributes: '' - cells: - - - tag: 'td' - attributes: '' - content: 'Chongqing' - - - tag: 'td' - attributes: '' - content: 'China' - - - tag: 'td' - attributes: '' - content: '14,838,000' - - - attributes: '' - cells: - - - tag: 'td' - attributes: '' - content: 'Istanbul' - - - tag: 'td' - attributes: '' - content: 'Turkey' - - - tag: 'td' - attributes: '' - content: '14,751,000' - - - attributes: '' - cells: - - - tag: 'td' - attributes: '' - content: 'Kolkata' - - - tag: 'td' - attributes: '' - content: 'India' - - - tag: 'td' - attributes: '' - content: '14,681,000' - - - attributes: '' - cells: - - - tag: 'td' - attributes: '' - content: 'Manila' - - - tag: 'td' - attributes: '' - content: 'Philippines' - - - tag: 'td' - attributes: '' - content: '13,482,000' - - - attributes: '' - cells: - - - tag: 'td' - attributes: '' - content: 'Lagos' - - - tag: 'td' - attributes: '' - content: 'Nigeria' - - - tag: 'td' - attributes: '' - content: '13,463,000' - - - attributes: '' - cells: - - - tag: 'td' - attributes: '' - content: 'Rio de Janeiro' - - - tag: 'td' - attributes: '' - content: 'Brazil' - - - tag: 'td' - attributes: '' - content: '13,293,000' - - - attributes: '' - cells: - - - tag: 'td' - attributes: '' - content: 'Tianjin' - - - tag: 'td' - attributes: '' - content: 'China' - - - tag: 'td' - attributes: '' - content: '13,215,000' diff --git a/source/06-utility/setScrollbarProperty.ts b/source/06-utility/setScrollbarProperty.ts new file mode 100644 index 00000000..bcb71bd6 --- /dev/null +++ b/source/06-utility/setScrollbarProperty.ts @@ -0,0 +1,30 @@ +/** + * Calculate the width of the vertical scrollbar. + * via https://codepen.io/Mamboleoo/post/scrollbars-and-css-custom-properties + */ +function calculateScrollbarSize(): number { + const containerWithScroll = document.createElement('div'); + containerWithScroll.style.visibility = 'hidden'; + containerWithScroll.style.overflow = 'scroll'; + const innerContainer = document.createElement('div'); + containerWithScroll.appendChild(innerContainer); + document.body.appendChild(containerWithScroll); + const width = containerWithScroll.offsetWidth - innerContainer.offsetWidth; + document.body.removeChild(containerWithScroll); + return width; +} + +/** + * Set a CSS variable with the width of the scrollbar. + */ +function setScrollbarProperty(): void { + const scrollbarWidth = calculateScrollbarSize(); + if (!Number.isNaN(scrollbarWidth)) { + document.documentElement.style.setProperty( + '--scrollbar-width', + `${scrollbarWidth}px`, + ); + } +} + +export default setScrollbarProperty;