Merge pull request #1154 from NoRedInk/perk/hackday-2022-11-04-regexes

use one pattern for generating IDs
This commit is contained in:
Tessa 2023-06-12 15:08:17 -06:00 committed by GitHub
commit 4cca69a123
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 121 additions and 199 deletions

View File

@ -23,6 +23,7 @@ import Html.Styled.Attributes as Attributes
import KeyboardSupport exposing (Key(..))
import Nri.Ui.Carousel.V1 as Carousel
import Nri.Ui.Colors.V1 as Colors
import Nri.Ui.Html.Attributes.V2 as Attributes
import Task
@ -217,11 +218,11 @@ toCarouselItem : Int -> a -> ( String, Carousel.Item Int msg )
toCarouselItem id _ =
let
idString =
String.fromInt id
Attributes.safeIdWithPrefix "slide" <| String.fromInt id
in
( [ "Carousel.buildItem"
, " { id = " ++ idString
, " , idString = \"" ++ idString ++ "-slide\""
, " { id = " ++ String.fromInt id
, " , idString = \"" ++ idString ++ "\""
, " , controlHtml = Html.text \"" ++ String.fromInt (id + 1) ++ "\""
, " , slideHtml = Html.text \"" ++ String.fromInt (id + 1) ++ " slide\""
, " }"
@ -229,7 +230,7 @@ toCarouselItem id _ =
|> String.join "\n "
, Carousel.buildItem
{ id = id
, idString = String.fromInt id ++ "-slide"
, idString = idString
, controlHtml = Html.text (String.fromInt (id + 1))
, slideHtml = Html.text (String.fromInt (id + 1) ++ " slide")
}

View File

@ -8,7 +8,7 @@ import Example exposing (Example)
import Html.Styled.Attributes as Attributes
import Nri.Ui.BreadCrumbs.V2 as BreadCrumbs exposing (BreadCrumbs)
import Nri.Ui.Header.V1 as Header
import Nri.Ui.Util exposing (dashify)
import Nri.Ui.Html.Attributes.V2 exposing (safeIdWithPrefix)
import Parser exposing ((|.), (|=), Parser)
import Url exposing (Url)
@ -168,7 +168,7 @@ categoryCrumb category_ =
doodadCrumb : BreadCrumbs (Route state msg) -> Example state msg -> BreadCrumbs (Route state msg)
doodadCrumb previous example =
BreadCrumbs.after previous
{ id = "breadcrumbs__" ++ dashify example.name
{ id = safeIdWithPrefix "breadcrumbs" example.name
, text = Example.fullName example
, route = Doodad example
}

View File

@ -65,7 +65,6 @@ import Nri.Ui.FocusRing.V1 as FocusRing
import Nri.Ui.Fonts.V1 as Fonts
import Nri.Ui.Html.Attributes.V2 as Extra
import Nri.Ui.Svg.V1 exposing (Svg)
import Nri.Ui.Util as Util
{-| This disables the input
@ -233,7 +232,7 @@ view { label, selected } attributes =
specificId
Nothing ->
"checkbox-v7-" ++ Util.safeIdString label
Extra.safeIdWithPrefix "checkbox-v7" label
config_ =
{ identifier = idValue

View File

@ -3,6 +3,7 @@ module Nri.Ui.Html.Attributes.V2 exposing
, targetBlank
, nriDescription, nriDescriptionSelector
, testId
, safeIdWithPrefix, safeId
)
{-|
@ -21,6 +22,7 @@ This is the new version of Nri.Ui.Html.Attributes.Extra.
@docs targetBlank
@docs nriDescription, nriDescriptionSelector
@docs testId
@docs safeIdWithPrefix, safeId
-}
@ -29,6 +31,7 @@ import Css.Global
import Html.Styled exposing (Attribute)
import Html.Styled.Attributes as Attributes
import Json.Encode as Encode
import Regex exposing (Regex)
{-| Represents an attribute with no semantic meaning, useful for conditionals.
@ -103,3 +106,47 @@ nriDescription description =
nriDescriptionSelector : String -> List Css.Style -> Css.Global.Snippet
nriDescriptionSelector description =
Css.Global.selector (String.join "" [ "[data-nri-description=\"", description, "\"]" ])
{-| Prepends a prefix to the result of safeId.
-}
safeIdWithPrefix : String -> String -> String
safeIdWithPrefix prefix string =
safeId
(prefix
++ "-"
++ string
)
{-| Creates a lowercased string that is safe to use for HTML IDs.
Removes all groups of unsafe characters and replaces each group with a dash.
prepends "id-" to the result to ensure that the ID starts with a letter. (necessary for CSS selectors including getElementById)
See code pen for examples: <https://codepen.io/ap-nri/pen/OJENQLY>
-}
safeId : String -> String
safeId unsafe =
let
nonAlphaNumUnderscoreHyphenAnywhere : Regex
nonAlphaNumUnderscoreHyphenAnywhere =
-- any contiguous block of characters that aren't any of
-- + ASCII letters
-- + numbers
-- + underscore
-- + the hyphen-minus character commonly called "dash" (seen in between "hyphen" and "minus" on this line)
-- This does not need the + at the end; Regex.replace is global by default
-- but we pay a penalty for calling the replacement function, so
-- calling it once per contiguous group is an easy way to cut down on that.
"[^a-zA-Z0-9_-]+"
|> Regex.fromString
|> Maybe.withDefault Regex.never
hyphenMinus : any -> String
hyphenMinus _ =
"-"
in
"id-"
++ Regex.replace
nonAlphaNumUnderscoreHyphenAnywhere
hyphenMinus
unsafe

View File

@ -53,9 +53,6 @@ import Nri.Ui.Fonts.V1 as Fonts
import Nri.Ui.Html.Attributes.V2 as Extra
import Nri.Ui.Pennant.V2 exposing (premiumFlag)
import Nri.Ui.Svg.V1 as Svg
import Nri.Ui.Util exposing (removePunctuation)
import String exposing (toLower)
import String.Extra exposing (dasherize)
{-| Set a custom ID for this checkbox and label. If you don't set this,
@ -205,7 +202,7 @@ view { label, onChange } attributes =
specificId
Nothing ->
"checkbox-" ++ dasherize (removePunctuation (toLower label))
Extra.safeIdWithPrefix "checkbox" label
isPremium =
config.premiumDisplay /= PremiumDisplay.Free

View File

@ -63,7 +63,6 @@ import Nri.Ui.Fonts.V1 as Fonts
import Nri.Ui.Html.Attributes.V2 as Extra
import Nri.Ui.Pennant.V2 as Pennant
import Nri.Ui.Svg.V1 exposing (Svg)
import Nri.Ui.Util as Util
import Svg.Styled as Svg
import Svg.Styled.Attributes as SvgAttributes
@ -277,7 +276,7 @@ view { label, name, value, valueToString, selectedValue } attributes =
specificId
Nothing ->
name ++ "-" ++ Util.safeIdString stringValue
Extra.safeId (name ++ "-" ++ stringValue)
isChecked =
selectedValue == Just value

View File

@ -31,9 +31,9 @@ import Nri.Ui.Colors.Extra as ColorsExtra
import Nri.Ui.Colors.V1 as Colors
import Nri.Ui.FocusRing.V1 as FocusRing
import Nri.Ui.Fonts.V1 as Fonts
import Nri.Ui.Html.Attributes.V2 exposing (safeId)
import Nri.Ui.Svg.V1 as Svg exposing (Svg)
import Nri.Ui.Tooltip.V3 as Tooltip
import Nri.Ui.Util exposing (dashify)
import TabsInternal.V2 as TabsInternal
@ -132,7 +132,7 @@ viewRadioGroup config =
)
name =
dashify (String.toLower config.legend)
safeId config.legend
legendId =
"legend-" ++ name

View File

@ -59,7 +59,6 @@ import Nri.Ui.CssVendorPrefix.V1 as VendorPrefixed
import Nri.Ui.Fonts.V1 as Fonts
import Nri.Ui.Html.Attributes.V2 as Extra
import Nri.Ui.InputStyles.V4 as InputStyles
import Nri.Ui.Util
import SolidColor
@ -475,8 +474,8 @@ viewChoice current choice =
{-| Pass in the label to generate the default DOM element id used by a `Select.view` with the given label.
-}
generateId : String -> String
generateId x =
"nri-select-" ++ Nri.Ui.Util.dashify (Nri.Ui.Util.removePunctuation x)
generateId =
Extra.safeIdWithPrefix "nri-select"
selectArrowsCss : { config | disabled : Bool } -> Css.Style

View File

@ -66,7 +66,6 @@ import Nri.Ui.Html.Attributes.V2 as Extra
import Nri.Ui.Html.V3 exposing (viewJust)
import Nri.Ui.InputStyles.V4 as InputStyles
import Nri.Ui.Svg.V1 as Svg exposing (Svg)
import Nri.Ui.Util
import SolidColor
@ -544,7 +543,7 @@ viewChoice current choice =
-}
generateId : String -> String
generateId x =
"nri-select-" ++ String.toLower (Nri.Ui.Util.dashify (Nri.Ui.Util.removePunctuation x))
Extra.safeIdWithPrefix "nri-select-" x
selectArrowsCss : { config | disabled : Bool } -> Css.Style

View File

@ -92,7 +92,6 @@ import InputLabelInternal
import Nri.Ui.Colors.V1 as Colors
import Nri.Ui.Html.Attributes.V2 as Extra
import Nri.Ui.InputStyles.V4 as InputStyles exposing (Theme(..))
import Nri.Ui.Util exposing (dashify, removePunctuation)
{-| This is private. The public API only exposes `Attribute`.
@ -458,5 +457,5 @@ writingSingleLineHeight =
{-| -}
generateId : String -> String
generateId labelText =
"nri-ui-text-area-" ++ (dashify <| removePunctuation labelText)
generateId =
Extra.safeIdWithPrefix "nri-ui-text-area"

View File

@ -72,7 +72,6 @@ import Nri.Ui.Html.Attributes.V2 as Extra
import Nri.Ui.InputStyles.V4 as InputStyles exposing (defaultMarginTop)
import Nri.Ui.Svg.V1 as Svg
import Nri.Ui.UiIcon.V1 as UiIcon
import Nri.Ui.Util exposing (dashify)
import Time
@ -982,8 +981,8 @@ view label attributes =
This is for use when you need the DOM element id for use in javascript (such as trigger an event to focus a particular text input)
-}
generateId : String -> String
generateId labelText =
"Nri-Ui-TextInput-" ++ dashify labelText
generateId =
Extra.safeIdWithPrefix "Nri-Ui-TextInput"
type alias FloatingContentConfig msg =

View File

@ -96,7 +96,6 @@ import Nri.Ui.Html.Attributes.V2 as ExtraAttributes
import Nri.Ui.MediaQuery.V1 as MediaQuery exposing (mobileBreakpoint, narrowMobileBreakpoint, quizEngineBreakpoint)
import Nri.Ui.Shadows.V1 as Shadows
import Nri.Ui.UiIcon.V1 as UiIcon
import Nri.Ui.Util as Util
import Nri.Ui.WhenFocusLeaves.V1 as WhenFocusLeaves
@ -883,7 +882,7 @@ viewToggleTip : { label : String, lastId : Maybe String } -> List (Attribute msg
viewToggleTip { label, lastId } attributes_ =
let
id =
Util.safeIdString label
ExtraAttributes.safeId label
triggerId =
"tooltip-trigger__" ++ id

View File

@ -1,89 +0,0 @@
module Nri.Ui.Util exposing (dashify, removePunctuation, safeIdString)
import Regex exposing (Regex)
{-| Convenience method for going from a string with spaces to a string with dashes.
-}
dashify : String -> String
dashify =
let
regex =
Regex.fromString " "
|> Maybe.withDefault Regex.never
in
Regex.replace regex (always "-")
{-| Convenience method for removing punctuation
(removes everything that isn't whitespace, alphanumeric, or an underscore).
-}
removePunctuation : String -> String
removePunctuation =
let
regex =
Regex.fromString "[^A-Za-z0-9\\w\\s]"
|> Maybe.withDefault Regex.never
in
Regex.replace regex (always "")
{-| Creates a lowercased string that is safe to use for HTML IDs.
Ensures that nonletter characters are cut from the front and replaces bad characters with a dash
-}
safeIdString : String -> String
safeIdString =
let
nonAlphaAtStart =
-- From the start of the string, match the contiguous block of
-- not ASCII letters at the start of the String.
-- Why [a-zA-Z] and not [A-z]?
-- There are punctuation characters in that range: [\]^_` all come after Z and before a
"^[^a-zA-Z]+"
nonAlphaNumUnderscoreHyphenAnywhere =
-- any contiguous block of characters that aren't any of
-- + ASCII letters
-- + numbers
-- + underscore
-- + the hyphen-minus character commonly called "dash" (seen in between "hyphen" and "minus" on this line)
-- This does not need the + at the end; Regex.replace is global by default
-- but we pay a penalty for calling the replacement function, so
-- calling it once per contiguous group is an easy way to cut down on that.
"[^a-zA-Z0-9_-]+"
anyOfThese strs =
"(" ++ String.join "|" strs ++ ")"
unsafeChar =
[ nonAlphaAtStart
, nonAlphaNumUnderscoreHyphenAnywhere
]
|> anyOfThese
|> regexFromString
nonAlphaNumAtEnd =
-- any contiguous block of letters that aren't any of
-- + ASCII letters
-- + numbers
regexFromString "[^a-zA-Z0-9]+$"
collapsePunctuationToOne =
regexFromString "[_-]+"
in
Regex.replace nonAlphaNumAtEnd (always "")
>> Regex.replace unsafeChar
(\{ index } ->
if index == 0 then
""
else
"-"
)
>> Regex.replace collapsePunctuationToOne (always "-")
>> String.toLower
regexFromString : String -> Regex
regexFromString =
Regex.fromString >> Maybe.withDefault Regex.never

View File

@ -16,7 +16,6 @@ import Html.Styled.Events as Events
import Html.Styled.Keyed as Keyed
import Json.Decode
import Nri.Ui.Html.Attributes.V2 as AttributesExtra
import Nri.Ui.Util exposing (dashify)
{-| -}
@ -53,7 +52,7 @@ viewTabs : Config id msg -> Html msg
viewTabs config =
Html.div
[ Role.tabList
, Aria.owns (List.map (tabToId << .idString) config.tabs)
, Aria.owns (List.map (AttributesExtra.safeId << .idString) config.tabs)
, Attributes.css config.tabListStyles
]
(List.map (viewTab_ config) config.tabs)
@ -103,7 +102,7 @@ viewTab_ config tab =
, Aria.selected isSelected
, Role.tab
, Aria.controls [ tabToBodyId tab.idString ]
, Attributes.id (tabToId tab.idString)
, Attributes.id (AttributesExtra.safeId tab.idString)
, Events.onFocus (config.onSelect tab.id)
, Events.on "keyup" <|
Json.Decode.andThen (keyEvents config tab) Events.keyCode
@ -121,7 +120,7 @@ keyEvents { onFocus, tabs } thisTab keyCode =
acc
( True, Nothing ) ->
( True, Just (tabToId tab.idString) )
( True, Just (AttributesExtra.safeId tab.idString) )
( False, Nothing ) ->
( tab.id == thisTab.id, Nothing )
@ -173,7 +172,7 @@ viewTabPanel : Tab id msg -> Bool -> Html msg
viewTabPanel tab selected =
Html.div
([ Role.tabPanel
, Aria.labelledBy (tabToId tab.idString)
, Aria.labelledBy (AttributesExtra.safeId tab.idString)
, Attributes.id (tabToBodyId tab.idString)
]
++ (if selected then
@ -191,16 +190,11 @@ viewTabPanel tab selected =
[ tab.panelView ]
tabToId : String -> String
tabToId tab =
dashify (String.toLower tab)
tabToBodyId : String -> String
tabToBodyId tab =
"tab-body-" ++ tabToId tab
tabToBodyId =
AttributesExtra.safeIdWithPrefix "tab-body"
tabToKeyedNode : String -> String
tabToKeyedNode tab =
"tabs-internal-keyed-node-" ++ tabToId tab
tabToKeyedNode =
AttributesExtra.safeIdWithPrefix "tabs-internal-keyed-node"

View File

@ -20,9 +20,8 @@ import Html.Styled.Attributes as Attributes
import Html.Styled.Events as Events
import Html.Styled.Keyed as Keyed
import Nri.Ui.FocusRing.V1 as FocusRing
import Nri.Ui.Html.Attributes.V2 as AttributesExtra
import Nri.Ui.Html.Attributes.V2 as AttributesExtra exposing (safeId, safeIdWithPrefix)
import Nri.Ui.Tooltip.V3 as Tooltip
import Nri.Ui.Util exposing (dashify)
{-| -}
@ -93,7 +92,7 @@ viewTabs config =
Html.div []
[ Html.div
[ Role.tabList
, Aria.owns (List.map (tabToId << .idString) config.tabs)
, Aria.owns (List.map (safeId << .idString) config.tabs)
]
[]
, Html.div [ Attributes.css config.tabListStyles ]
@ -161,7 +160,7 @@ viewTab_ config index tab =
, Aria.selected isSelected
, Role.tab
, Aria.controls [ tabToBodyId tab.idString ]
, Attributes.id (tabToId tab.idString)
, Attributes.id (safeId tab.idString)
, Key.onKeyUpPreventDefault (keyEvents config tab)
]
++ (case tab.labelledBy of
@ -193,7 +192,7 @@ viewTab_ config index tab =
( Nothing, tooltipAttributes ) ->
Tooltip.view
{ id = "tab-tooltip__" ++ tabToId tab.idString
{ id = safeIdWithPrefix "tab-tooltip" tab.idString
, trigger = \eventHandlers -> buttonOrLink eventHandlers
}
([ Tooltip.smallPadding
@ -209,7 +208,7 @@ keyEvents { focusAndSelect, tabs } thisTab =
let
onFocus : Tab id msg -> msg
onFocus tab =
focusAndSelect { select = tab.id, focus = Just (tabToId tab.idString) }
focusAndSelect { select = tab.id, focus = Just (safeId tab.idString) }
findAdjacentTab : Tab id msg -> ( Bool, Maybe msg ) -> ( Bool, Maybe msg )
findAdjacentTab tab ( isAdjacentTab, acc ) =
@ -265,7 +264,7 @@ viewTabPanel : Tab id msg -> Bool -> Html msg
viewTabPanel tab selected =
Html.div
([ Role.tabPanel
, Aria.labelledBy (tabToId tab.idString)
, Aria.labelledBy (safeId tab.idString)
, Attributes.id (tabToBodyId tab.idString)
, Attributes.tabindex 0
, Attributes.class FocusRing.customClass
@ -287,16 +286,11 @@ viewTabPanel tab selected =
[ tab.panelView ]
tabToId : String -> String
tabToId tab =
dashify (String.toLower tab)
tabToBodyId : String -> String
tabToBodyId tab =
"tab-body-" ++ tabToId tab
tabToBodyId =
safeIdWithPrefix "tab-body"
tabToKeyedNode : String -> String
tabToKeyedNode tab =
"tabs-internal-keyed-node-" ++ tabToId tab
tabToKeyedNode =
safeIdWithPrefix "tabs-internal-keyed-node"

View File

@ -0,0 +1,29 @@
module Spec.Nri.Ui.Html.Attributes exposing (spec)
import Expect
import Nri.Ui.Html.Attributes.V2 as Attributes
import Test exposing (..)
spec : Test
spec =
describe "Nri.Ui.Html.Attributes"
[ describe "safeId transforms strings as expected"
[ test "with an apostrophe and multiple spaces" <|
\() ->
Attributes.safeId "Enable text-to-speech for \t Angela's account"
|> Expect.equal "id-Enable-text-to-speech-for-Angela-s-account"
, test "lotsa hyphens and dashes" <|
\() ->
Attributes.safeId "--__--hellO----_______---HOw----___---____--ArE------___You___--__--__Today"
|> Expect.equal "id---__--hellO----_______---HOw----___---____--ArE------___You___--__--__Today"
]
, describe "safeIdWithPrefix"
[ test "test everything at once" <|
\() ->
Attributes.safeIdWithPrefix
"000321 ¡¡VERY!! unsafe "
"0--__--hellO----___!?[]____---HOw----___---____--ArE------___You___--__--__Today?"
|> Expect.equal "id-000321-VERY-unsafe--0--__--hellO----___-____---HOw----___---____--ArE------___You___--__--__Today-"
]
]

View File

@ -16,19 +16,19 @@ spec =
ungrouped
-- first option is selected automatically
|> ensureViewHas
[ id "nri-select-cat"
[ id "id-nri-select--Cat"
, selected True
, attribute (Attributes.value "Cat")
]
|> selectAnimal Dog
|> ensureViewHas
[ id "nri-select-dog"
[ id "id-nri-select--Dog"
, selected True
, attribute (Attributes.value "Dog")
]
|> selectAnimal Other
|> ensureViewHas
[ id "nri-select-my-favorite-animal-is-something-else"
[ id "id-nri-select--My-favorite-animal-is-something-else"
, selected True
, attribute (Attributes.value "My favorite animal is something else")
]
@ -38,25 +38,25 @@ spec =
grouped
-- first option is selected automatically
|> ensureViewHas
[ id "nri-select-cat"
[ id "id-nri-select--Cat"
, selected True
, attribute (Attributes.value "Cat")
]
|> selectAnimal Dog
|> ensureViewHas
[ id "nri-select-dog"
[ id "id-nri-select--Dog"
, selected True
, attribute (Attributes.value "Dog")
]
|> selectAnimal Axolotl
|> ensureViewHas
[ id "nri-select-axolotl"
[ id "id-nri-select--Axolotl"
, selected True
, attribute (Attributes.value "Axolotl")
]
|> selectAnimal Other
|> ensureViewHas
[ id "nri-select-my-favorite-animal-is-something-else"
[ id "id-nri-select--My-favorite-animal-is-something-else"
, selected True
, attribute (Attributes.value "My favorite animal is something else")
]

View File

@ -1,44 +0,0 @@
module Spec.Nri.Ui.Util exposing (spec)
import Expect
import Nri.Ui.Util as Util
import Test exposing (..)
spec : Test
spec =
describe "Nri.Ui.Util"
[ describe "safeIdString transforms strings as expected"
[ test "with a comma" <|
\() ->
Util.safeIdString "Enable text-to-speech for Angela's account"
|> Expect.equal "enable-text-to-speech-for-angela-s-account"
, test "removes leading non alpha characters" <|
\() ->
Util.safeIdString "#@!!232now we are in business__--__"
|> Expect.equal "now-we-are-in-business"
, test "removes trailing non alphanum characters" <|
\() ->
Util.safeIdString "#@!!232now we are in business__--__123!!@&*%^ @"
|> Expect.equal "now-we-are-in-business-123"
, test "hard mode" <|
\() ->
Util.safeIdString "!@#21something else interesting321$ ... hi"
|> Expect.equal "something-else-interesting321-hi"
, test "with capital letters" <|
\() ->
Util.safeIdString "1232!@#%#@JFEKLfds-----SFJK3@#@jj23FDS........''''\"\"***"
|> Expect.equal "jfeklfds-sfjk3-jj23fds"
, test "lotsa hyphens and dashes" <|
\() ->
Util.safeIdString "--__--hellO----_______---HOw----___---____--ArE------___You___--__--__Today"
|> Expect.equal "hello-how-are-you-today"
]
, describe "removePunctuation"
[ test "A string with some punctuation" <|
\() ->
Util.removePunctuation
"To sleep? Perchance: to dream? Alas poor Yorick but he's not `in` _this_ \"[play]\"."
|> Expect.equal "To sleep Perchance to dream Alas poor Yorick but hes not in _this_ play"
]
]